diff --git a/command/apply.go b/command/apply.go index 84d7aad041..35dcde348c 100644 --- a/command/apply.go +++ b/command/apply.go @@ -2,7 +2,6 @@ package command import ( "bytes" - "flag" "fmt" "os" "sort" @@ -25,7 +24,7 @@ func (c *ApplyCommand) Run(args []string) int { args = c.Meta.process(args) - cmdFlags := flag.NewFlagSet("apply", flag.ContinueOnError) + cmdFlags := c.Meta.flagSet("apply") cmdFlags.BoolVar(&init, "init", false, "init") cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") @@ -207,6 +206,14 @@ Options: "-state". This can be used to preserve the old state. + -var 'foo=bar' Set a variable in the Terraform configuration. This + flag can be set multiple times. + + -var-file=foo Set variables in the Terraform configuration from + a file. If "terraform.tfvars" is present, it will be + automatically loaded if this flag is not specified. + + ` return strings.TrimSpace(helpText) } diff --git a/command/apply_test.go b/command/apply_test.go index 05a5c299e8..db1ee949a9 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -466,3 +466,41 @@ func TestApply_stateNoExist(t *testing.T) { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) } } + +func TestApply_vars(t *testing.T) { + statePath := testTempFile(t) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + actual := "" + p.DiffFn = func( + s *terraform.ResourceState, + c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) { + if v, ok := c.Config["value"]; ok { + actual = v.(string) + } + + return nil, nil + } + + args := []string{ + "-init", + "-var", "foo=bar", + "-state", statePath, + testFixturePath("apply-vars"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if actual != "bar" { + t.Fatal("didn't work") + } +} diff --git a/command/flag_var.go b/command/flag_var.go new file mode 100644 index 0000000000..8d54f20c4a --- /dev/null +++ b/command/flag_var.go @@ -0,0 +1,42 @@ +package command + +import ( + "fmt" + "strings" +) + +// FlagVar is a flag.Value implementation for parsing user variables +// from the command-line in the format of '-var key=value'. +type FlagVar map[string]string + +func (v *FlagVar) String() string { + return "" +} + +func (v *FlagVar) Set(raw string) error { + idx := strings.Index(raw, "=") + if idx == -1 { + return fmt.Errorf("No '=' value in arg: %s", raw) + } + + if *v == nil { + *v = make(map[string]string) + } + + key, value := raw[0:idx], raw[idx+1:] + (*v)[key] = value + return nil +} + +// FlagVarFile is a flag.Value implementation for parsing user variables +// from the command line in the form of files. i.e. '-var-file=foo' +type FlagVarFile map[string]string + +func (v *FlagVarFile) String() string { + return "" +} + +func (v *FlagVarFile) Set(raw string) error { + // TODO + return nil +} diff --git a/command/flag_var_test.go b/command/flag_var_test.go new file mode 100644 index 0000000000..1177b325c6 --- /dev/null +++ b/command/flag_var_test.go @@ -0,0 +1,56 @@ +package command + +import ( + "flag" + "reflect" + "testing" +) + +func TestFlagVar_impl(t *testing.T) { + var _ flag.Value = new(FlagVar) +} + +func TestFlagVar(t *testing.T) { + cases := []struct { + Input string + Output map[string]string + Error bool + }{ + { + "key=value", + map[string]string{"key": "value"}, + false, + }, + + { + "key=", + map[string]string{"key": ""}, + false, + }, + + { + "key=foo=bar", + map[string]string{"key": "foo=bar"}, + false, + }, + + { + "key", + nil, + true, + }, + } + + for _, tc := range cases { + f := new(FlagVar) + err := f.Set(tc.Input) + if (err != nil) != tc.Error { + t.Fatalf("bad error. Input: %#v", tc.Input) + } + + actual := map[string]string(*f) + if !reflect.DeepEqual(actual, tc.Output) { + t.Fatalf("bad: %#v", actual) + } + } +} diff --git a/command/meta.go b/command/meta.go index 2d00627b85..297557e7cd 100644 --- a/command/meta.go +++ b/command/meta.go @@ -1,6 +1,7 @@ package command import ( + "flag" "fmt" "os" @@ -19,6 +20,9 @@ type Meta struct { // This can be set by the command itself to provide extra hooks. extraHooks []terraform.Hook + // Variables for the context (private) + variables map[string]string + color bool oldUi cli.Ui } @@ -109,9 +113,29 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { copy(opts.Hooks[1:], m.ContextOpts.Hooks) copy(opts.Hooks[len(m.ContextOpts.Hooks)+1:], m.extraHooks) println(fmt.Sprintf("%#v", opts.Hooks)) + + if len(m.variables) > 0 { + vs := make(map[string]string) + for k, v := range opts.Variables { + vs[k] = v + } + for k, v := range m.variables { + vs[k] = v + } + opts.Variables = vs + } + return &opts } +// flags adds the meta flags to the given FlagSet. +func (m *Meta) flagSet(n string) *flag.FlagSet { + f := flag.NewFlagSet(n, flag.ContinueOnError) + f.Var((*FlagVar)(&m.variables), "var", "variables") + f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file") + return f +} + // process will process the meta-parameters out of the arguments. This // will potentially modify the args in-place. It will return the resulting // slice. diff --git a/command/plan.go b/command/plan.go index 7808c3879c..5404098162 100644 --- a/command/plan.go +++ b/command/plan.go @@ -1,7 +1,6 @@ package command import ( - "flag" "fmt" "log" "os" @@ -22,7 +21,7 @@ func (c *PlanCommand) Run(args []string) int { args = c.Meta.process(args) - cmdFlags := flag.NewFlagSet("plan", flag.ContinueOnError) + cmdFlags := c.Meta.flagSet("plan") cmdFlags.BoolVar(&destroy, "destroy", false, "destroy") cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") cmdFlags.StringVar(&outPath, "out", "", "path") @@ -145,6 +144,13 @@ Options: up Terraform-managed resources. By default it will use the state "terraform.tfstate" if it exists. + -var 'foo=bar' Set a variable in the Terraform configuration. This + flag can be set multiple times. + + -var-file=foo Set variables in the Terraform configuration from + a file. If "terraform.tfvars" is present, it will be + automatically loaded if this flag is not specified. + ` return strings.TrimSpace(helpText) } diff --git a/command/plan_test.go b/command/plan_test.go index 473f68a0ce..5a5778736c 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -281,3 +281,37 @@ func TestPlan_stateDefault(t *testing.T) { t.Fatalf("bad: %#v", p.DiffState) } } + +func TestPlan_vars(t *testing.T) { + p := testProvider() + ui := new(cli.MockUi) + c := &PlanCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + actual := "" + p.DiffFn = func( + s *terraform.ResourceState, + c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) { + if v, ok := c.Config["value"]; ok { + actual = v.(string) + } + + return nil, nil + } + + args := []string{ + "-var", "foo=bar", + testFixturePath("plan-vars"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if actual != "bar" { + t.Fatal("didn't work") + } +} diff --git a/command/refresh.go b/command/refresh.go index 39b27530ac..574964f75d 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -1,7 +1,6 @@ package command import ( - "flag" "fmt" "log" "os" @@ -21,7 +20,7 @@ func (c *RefreshCommand) Run(args []string) int { args = c.Meta.process(args) - cmdFlags := flag.NewFlagSet("refresh", flag.ContinueOnError) + cmdFlags := c.Meta.flagSet("refresh") cmdFlags.StringVar(&statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&stateOutPath, "state-out", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } @@ -128,6 +127,13 @@ Options: -state-out=path Path to write updated state file. By default, the "-state" path will be used. + -var 'foo=bar' Set a variable in the Terraform configuration. This + flag can be set multiple times. + + -var-file=foo Set variables in the Terraform configuration from + a file. If "terraform.tfvars" is present, it will be + automatically loaded if this flag is not specified. + ` return strings.TrimSpace(helpText) } diff --git a/command/refresh_test.go b/command/refresh_test.go index 7acf39e5fe..0f583cfef8 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -296,3 +296,40 @@ func TestRefresh_outPath(t *testing.T) { t.Fatalf("bad: %#v", actual) } } + +func TestRefresh_var(t *testing.T) { + state := &terraform.State{ + Resources: map[string]*terraform.ResourceState{ + "test_instance.foo": &terraform.ResourceState{ + ID: "bar", + Type: "test_instance", + }, + }, + } + statePath := testStateFile(t, state) + + p := testProvider() + ui := new(cli.MockUi) + c := &RefreshCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-var", "foo=bar", + "-state", statePath, + testFixturePath("refresh-var"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if !p.ConfigureCalled { + t.Fatal("configure should be called") + } + if p.ConfigureConfig.Config["value"].(string) != "bar" { + t.Fatalf("bad: %#v", p.ConfigureConfig.Config) + } +} diff --git a/command/test-fixtures/apply-vars/main.tf b/command/test-fixtures/apply-vars/main.tf new file mode 100644 index 0000000000..6c1541d3a0 --- /dev/null +++ b/command/test-fixtures/apply-vars/main.tf @@ -0,0 +1,5 @@ +variable "foo" {} + +resource "test_instance" "foo" { + value = "${var.foo}" +} diff --git a/command/test-fixtures/plan-vars/main.tf b/command/test-fixtures/plan-vars/main.tf new file mode 100644 index 0000000000..6c1541d3a0 --- /dev/null +++ b/command/test-fixtures/plan-vars/main.tf @@ -0,0 +1,5 @@ +variable "foo" {} + +resource "test_instance" "foo" { + value = "${var.foo}" +} diff --git a/command/test-fixtures/refresh-var/main.tf b/command/test-fixtures/refresh-var/main.tf new file mode 100644 index 0000000000..daabae5cc6 --- /dev/null +++ b/command/test-fixtures/refresh-var/main.tf @@ -0,0 +1,7 @@ +variable "foo" {} + +provider "test" { + value = "${var.foo}" +} + +resource "test_instance" "foo" {}