diff --git a/common/command/build_flags.go b/common/command/build_flags.go index 167e13310..d08ca58b8 100644 --- a/common/command/build_flags.go +++ b/common/command/build_flags.go @@ -12,6 +12,7 @@ func BuildOptionFlags(fs *flag.FlagSet, f *BuildOptions) { fs.Var((*SliceValue)(&f.Except), "except", "build all builds except these") fs.Var((*SliceValue)(&f.Only), "only", "only build the given builds by name") fs.Var((*userVarValue)(&f.UserVars), "var", "specify a user variable") + fs.Var((*AppendSliceValue)(&f.UserVarFiles), "var-file", "file with user variables") } // userVarValue is a flag.Value that parses out user variables in diff --git a/common/command/build_flags_test.go b/common/command/build_flags_test.go index b4f9753fe..5d39eb946 100644 --- a/common/command/build_flags_test.go +++ b/common/command/build_flags_test.go @@ -17,6 +17,8 @@ func TestBuildOptionFlags(t *testing.T) { "-var=foo=bar", "-var", "bar=baz", "-var=foo=bang", + "-var-file=foo", + "-var-file=bar", } err := fs.Parse(args) @@ -45,6 +47,11 @@ func TestBuildOptionFlags(t *testing.T) { if opts.UserVars["bar"] != "baz" { t.Fatalf("bad: %#v", opts.UserVars) } + + expected = []string{"foo", "bar"} + if !reflect.DeepEqual(opts.UserVarFiles, expected) { + t.Fatalf("bad: %#v", opts.UserVarFiles) + } } func TestUserVarValue_implements(t *testing.T) { diff --git a/common/command/flag_slice_value.go b/common/command/flag_slice_value.go index b6f466463..8989dedad 100644 --- a/common/command/flag_slice_value.go +++ b/common/command/flag_slice_value.go @@ -2,6 +2,23 @@ package command import "strings" +// AppendSliceValue implements the flag.Value interface and allows multiple +// calls to the same variable to append a list. +type AppendSliceValue []string + +func (s *AppendSliceValue) String() string { + return strings.Join(*s, ",") +} + +func (s *AppendSliceValue) Set(value string) error { + if *s == nil { + *s = make([]string, 0, 1) + } + + *s = append(*s, value) + return nil +} + // SliceValue implements the flag.Value interface and allows a list of // strings to be given on the command line and properly parsed into a slice // of strings internally. diff --git a/common/command/flag_slice_value_test.go b/common/command/flag_slice_value_test.go index 1b9e2a762..ca80c9d9f 100644 --- a/common/command/flag_slice_value_test.go +++ b/common/command/flag_slice_value_test.go @@ -6,6 +6,32 @@ import ( "testing" ) +func TestAppendSliceValue_implements(t *testing.T) { + var raw interface{} + raw = new(AppendSliceValue) + if _, ok := raw.(flag.Value); !ok { + t.Fatalf("AppendSliceValue should be a Value") + } +} + +func TestAppendSliceValueSet(t *testing.T) { + sv := new(AppendSliceValue) + err := sv.Set("foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + err = sv.Set("bar") + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := []string{"foo", "bar"} + if !reflect.DeepEqual([]string(*sv), expected) { + t.Fatalf("Bad: %#v", sv) + } +} + func TestSliceValue_implements(t *testing.T) { var raw interface{} raw = new(SliceValue) diff --git a/common/command/template.go b/common/command/template.go index 3f7a8b9c1..4c14dd038 100644 --- a/common/command/template.go +++ b/common/command/template.go @@ -1,18 +1,22 @@ package command import ( + "encoding/json" "errors" "fmt" "github.com/mitchellh/packer/packer" + "io/ioutil" "log" + "os" ) // BuildOptions is a set of options related to builds that can be set // from the command line. type BuildOptions struct { - UserVars map[string]string - Except []string - Only []string + UserVarFiles []string + UserVars map[string]string + Except []string + Only []string } // Validate validates the options @@ -21,6 +25,14 @@ func (f *BuildOptions) Validate() error { return errors.New("Only one of '-except' or '-only' may be specified.") } + if len(f.UserVarFiles) > 0 { + for _, path := range f.UserVarFiles { + if _, err := os.Stat(path); err != nil { + return fmt.Errorf("Cannot access: %s", path) + } + } + } + return nil } @@ -29,6 +41,18 @@ func (f *BuildOptions) Validate() error { func (f *BuildOptions) AllUserVars() (map[string]string, error) { all := make(map[string]string) + // Copy in the variables from the files + for _, path := range f.UserVarFiles { + fileVars, err := readFileVars(path) + if err != nil { + return nil, err + } + + for k, v := range fileVars { + all[k] = v + } + } + // Copy in the command-line vars for k, v := range f.UserVars { all[k] = v @@ -84,3 +108,18 @@ func (f *BuildOptions) Builds(t *packer.Template, cf *packer.ComponentFinder) ([ return builds, nil } + +func readFileVars(path string) (map[string]string, error) { + bytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + vars := make(map[string]string) + err = json.Unmarshal(bytes, &vars) + if err != nil { + return nil, err + } + + return vars, nil +} diff --git a/common/command/template_test.go b/common/command/template_test.go index 18a9805db..ae1bd6ea3 100644 --- a/common/command/template_test.go +++ b/common/command/template_test.go @@ -35,3 +35,19 @@ func TestBuildOptionsValidate(t *testing.T) { t.Fatalf("err: %s", err) } } + +func TestBuildOptionsValidate_userVarFiles(t *testing.T) { + bf := new(BuildOptions) + + err := bf.Validate() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Non-existent file + bf.UserVarFiles = []string{"ireallyshouldntexistanywhere"} + err = bf.Validate() + if err == nil { + t.Fatal("should error") + } +}