From 95890003b751e1a976f7cb2d87284dc3a0b18515 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 19 May 2015 15:25:56 -0600 Subject: [PATCH] template: builder parsing --- template/parse.go | 123 ++++++++++++++++++ template/parse_test.go | 55 ++++++++ template/template.go | 77 +++++++++++ template/template_test.go | 12 ++ template/test-fixtures/parse-basic.json | 3 + .../test-fixtures/parse-builder-no-type.json | 3 + .../test-fixtures/parse-builder-repeat.json | 6 + 7 files changed, 279 insertions(+) create mode 100644 template/parse.go create mode 100644 template/parse_test.go create mode 100644 template/template.go create mode 100644 template/template_test.go create mode 100644 template/test-fixtures/parse-basic.json create mode 100644 template/test-fixtures/parse-builder-no-type.json create mode 100644 template/test-fixtures/parse-builder-repeat.json diff --git a/template/parse.go b/template/parse.go new file mode 100644 index 000000000..3cb9e288e --- /dev/null +++ b/template/parse.go @@ -0,0 +1,123 @@ +package template + +import ( + "encoding/json" + "fmt" + "io" + "sort" + + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/mapstructure" +) + +// rawTemplate is the direct JSON document format of the template file. +// This is what is decoded directly from the file, and then it is turned +// into a Template object thereafter. +type rawTemplate struct { + MinVersion string `mapstructure:"min_packer_version"` + Description string + + Builders []map[string]interface{} + Push map[string]interface{} + PostProcesors []interface{} `mapstructure:"post-processors"` + Provisioners []map[string]interface{} + Variables map[string]interface{} +} + +// Template returns the actual Template object built from this raw +// structure. +func (r *rawTemplate) Template() (*Template, error) { + var result Template + var errs error + + // Let's start by gathering all the builders + result.Builders = make(map[string]*Builder) + for i, rawB := range r.Builders { + var b Builder + if err := mapstructure.WeakDecode(rawB, &b); err != nil { + errs = multierror.Append(errs, fmt.Errorf( + "builder %d: %s", i+1, err)) + continue + } + + // Set the raw configuration and delete any special keys + b.Config = rawB + delete(b.Config, "name") + delete(b.Config, "type") + if len(b.Config) == 0 { + b.Config = nil + } + + // If there is no type set, it is an error + if b.Type == "" { + errs = multierror.Append(errs, fmt.Errorf( + "builder %d: missing 'type'", i+1)) + continue + } + + // The name defaults to the type if it isn't set + if b.Name == "" { + b.Name = b.Type + } + + // If this builder already exists, it is an error + if _, ok := result.Builders[b.Name]; ok { + errs = multierror.Append(errs, fmt.Errorf( + "builder %d: builder with name '%s' already exists", + i+1, b.Name)) + continue + } + + // Append the builders + result.Builders[b.Name] = &b + } + + // If we have errors, return those with a nil result + if errs != nil { + return nil, errs + } + + return &result, nil +} + +// Parse takes the given io.Reader and parses a Template object out of it. +func Parse(r io.Reader) (*Template, error) { + // First, decode the object into an interface{}. We do this instead of + // the rawTemplate directly because we'd rather use mapstructure to + // decode since it has richer errors. + var raw interface{} + if err := json.NewDecoder(r).Decode(&raw); err != nil { + return nil, err + } + + // Create our decoder + var md mapstructure.Metadata + var rawTpl rawTemplate + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Metadata: &md, + Result: &rawTpl, + }) + if err != nil { + return nil, err + } + + // Do the actual decode into our structure + if err := decoder.Decode(raw); err != nil { + return nil, err + } + + // Build an error if there are unused root level keys + if len(md.Unused) > 0 { + sort.Strings(md.Unused) + for _, unused := range md.Unused { + err = multierror.Append(err, fmt.Errorf( + "Unknown root level key in template: '%s'", unused)) + } + + // Return early for these errors + return nil, err + } + + // Return the template parsed from the raw structure + return rawTpl.Template() +} diff --git a/template/parse_test.go b/template/parse_test.go new file mode 100644 index 000000000..b7789298c --- /dev/null +++ b/template/parse_test.go @@ -0,0 +1,55 @@ +package template + +import ( + "os" + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + cases := []struct { + File string + Result *Template + Err bool + }{ + { + "parse-basic.json", + &Template{ + Builders: map[string]*Builder{ + "something": &Builder{ + Name: "something", + Type: "something", + }, + }, + }, + false, + }, + { + "parse-builder-no-type.json", + nil, + true, + }, + { + "parse-builder-repeat.json", + nil, + true, + }, + } + + for _, tc := range cases { + f, err := os.Open(fixtureDir(tc.File)) + if err != nil { + t.Fatalf("err: %s", err) + } + + tpl, err := Parse(f) + f.Close() + if (err != nil) != tc.Err { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(tpl, tc.Result) { + t.Fatalf("bad: %#v", tpl) + } + } +} diff --git a/template/template.go b/template/template.go new file mode 100644 index 000000000..477a6d824 --- /dev/null +++ b/template/template.go @@ -0,0 +1,77 @@ +package template + +import ( + "fmt" + "time" +) + +// Template represents the parsed template that is used to configure +// Packer builds. +type Template struct { + Description string + MinVersion string + + Variables map[string]*Variable + Builders map[string]*Builder + Provisioners []*Provisioner + PostProcessors [][]*PostProcessor + Push *Push +} + +// Builder represents a builder configured in the template +type Builder struct { + Name string + Type string + Config map[string]interface{} +} + +// PostProcessor represents a post-processor within the template. +type PostProcessor struct { + OnlyExcept + + Type string + KeepInputArtifact bool + Config map[string]interface{} +} + +// Provisioner represents a provisioner within the template. +type Provisioner struct { + OnlyExcept + + Type string + Config map[string]interface{} + Override map[string]interface{} + PauseBefore time.Duration +} + +// Push represents the configuration for pushing the template to Atlas. +type Push struct { + Name string + Address string + BaseDir string `mapstructure:"base_dir"` + Include []string + Exclude []string + Token string + VCS bool +} + +// Variable represents a variable within the template +type Variable struct { + Default string + Required bool +} + +// OnlyExcept is a struct that is meant to be embedded that contains the +// logic required for "only" and "except" meta-parameters. +type OnlyExcept struct { + Only []string + Except []string +} + +//------------------------------------------------------------------- +// GoStringer +//------------------------------------------------------------------- + +func (b *Builder) GoString() string { + return fmt.Sprintf("*%#v", *b) +} diff --git a/template/template_test.go b/template/template_test.go new file mode 100644 index 000000000..2847bf9a2 --- /dev/null +++ b/template/template_test.go @@ -0,0 +1,12 @@ +package template + +import ( + "path/filepath" +) + +const FixturesDir = "./test-fixtures" + +// fixtureDir returns the path to a test fixtures directory +func fixtureDir(n string) string { + return filepath.Join(FixturesDir, n) +} diff --git a/template/test-fixtures/parse-basic.json b/template/test-fixtures/parse-basic.json new file mode 100644 index 000000000..43b7a7898 --- /dev/null +++ b/template/test-fixtures/parse-basic.json @@ -0,0 +1,3 @@ +{ + "builders": [{"type": "something"}] +} diff --git a/template/test-fixtures/parse-builder-no-type.json b/template/test-fixtures/parse-builder-no-type.json new file mode 100644 index 000000000..1729d0827 --- /dev/null +++ b/template/test-fixtures/parse-builder-no-type.json @@ -0,0 +1,3 @@ +{ + "builders": [{"foo": "something"}] +} diff --git a/template/test-fixtures/parse-builder-repeat.json b/template/test-fixtures/parse-builder-repeat.json new file mode 100644 index 000000000..258b75883 --- /dev/null +++ b/template/test-fixtures/parse-builder-repeat.json @@ -0,0 +1,6 @@ +{ + "builders": [ + {"type": "something"}, + {"type": "something"} + ] +}