From df8529719c19ea2744147ab58a35f2f91611d20b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Mar 2017 23:27:05 -0700 Subject: [PATCH] command/init: backend-config accepts key=value pairs This augments backend-config to also accept key=value pairs. This should make Terraform easier to script rather than having to generate a JSON file. You must still specify the backend type as a minimal amount in configurations, example: ``` terraform { backend "consul" {} } ``` This is required because Terraform needs to be able to detect the _absense_ of that value for unsetting, if that is necessary at some point. --- command/init.go | 19 +- command/meta_backend.go | 18 ++ helper/variables/flag_any.go | 25 ++ helper/variables/flag_any_test.go | 299 ++++++++++++++++++ website/source/docs/backends/config.html.md | 7 +- .../source/docs/commands/init.html.markdown | 33 +- 6 files changed, 389 insertions(+), 12 deletions(-) create mode 100644 helper/variables/flag_any.go create mode 100644 helper/variables/flag_any_test.go diff --git a/command/init.go b/command/init.go index d20e883bc2..5347344459 100644 --- a/command/init.go +++ b/command/init.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-getter" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/helper/variables" ) // InitCommand is a Command implementation that takes a Terraform @@ -19,11 +20,11 @@ type InitCommand struct { func (c *InitCommand) Run(args []string) int { var flagBackend, flagGet bool - var flagConfigFile string + var flagConfigExtra map[string]interface{} args = c.Meta.process(args, false) cmdFlags := c.flagSet("init") cmdFlags.BoolVar(&flagBackend, "backend", true, "") - cmdFlags.StringVar(&flagConfigFile, "backend-config", "", "") + cmdFlags.Var((*variables.FlagAny)(&flagConfigExtra), "backend-config", "") cmdFlags.BoolVar(&flagGet, "get", true, "") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { @@ -138,9 +139,9 @@ func (c *InitCommand) Run(args []string) int { } opts := &BackendOpts{ - ConfigPath: path, - ConfigFile: flagConfigFile, - Init: true, + ConfigPath: path, + ConfigExtra: flagConfigExtra, + Init: true, } if _, err := c.Backend(opts); err != nil { c.Ui.Error(err.Error()) @@ -210,8 +211,12 @@ Options: -backend=true Configure the backend for this environment. - -backend-config=path A path to load additional configuration for the backend. - This is merged with what is in the configuration file. + -backend-config=path This can be either a path to an HCL file with key/value + assignments (same format as terraform.tfvars) or a + 'key=value' format. This is merged with what is in the + configuration file. This can be specified multiple + times. The backend type must be in the configuration + itself. -get=true Download any modules for this configuration. diff --git a/command/meta_backend.go b/command/meta_backend.go index 62029278e7..79466a380d 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -38,6 +38,10 @@ type BackendOpts struct { // from a file. ConfigFile string + // ConfigExtra is extra configuration to merge into the backend + // configuration after the extra file above. + ConfigExtra map[string]interface{} + // Plan is a plan that is being used. If this is set, the backend // configuration and output configuration will come from this plan. Plan *terraform.Plan @@ -251,6 +255,20 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*config.Backend, error) { backend.RawConfig = backend.RawConfig.Merge(rc) } + // If we have extra config values, merge that + if len(opts.ConfigExtra) > 0 { + log.Printf( + "[DEBUG] command: adding extra backend config from CLI") + rc, err := config.NewRawConfig(opts.ConfigExtra) + if err != nil { + return nil, fmt.Errorf( + "Error adding extra configuration file for backend: %s", err) + } + + // Merge in the configuration + backend.RawConfig = backend.RawConfig.Merge(rc) + } + // Validate the backend early. We have to do this before the normal // config validation pass since backend loading happens earlier. if errs := backend.Validate(); len(errs) > 0 { diff --git a/helper/variables/flag_any.go b/helper/variables/flag_any.go new file mode 100644 index 0000000000..650324e434 --- /dev/null +++ b/helper/variables/flag_any.go @@ -0,0 +1,25 @@ +package variables + +import ( + "strings" +) + +// FlagAny is a flag.Value for parsing user variables in the format of +// 'key=value' OR a file path. 'key=value' is assumed if '=' is in the value. +// You cannot use a file path that contains an '='. +type FlagAny map[string]interface{} + +func (v *FlagAny) String() string { + return "" +} + +func (v *FlagAny) Set(raw string) error { + idx := strings.Index(raw, "=") + if idx >= 0 { + flag := (*Flag)(v) + return flag.Set(raw) + } + + flag := (*FlagFile)(v) + return flag.Set(raw) +} diff --git a/helper/variables/flag_any_test.go b/helper/variables/flag_any_test.go new file mode 100644 index 0000000000..8cf72fcad6 --- /dev/null +++ b/helper/variables/flag_any_test.go @@ -0,0 +1,299 @@ +package variables + +import ( + "flag" + "fmt" + "io/ioutil" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestFlagAny_impl(t *testing.T) { + var _ flag.Value = new(FlagAny) +} + +func TestFlagAny(t *testing.T) { + cases := []struct { + Input interface{} + Output map[string]interface{} + Error bool + }{ + { + "=value", + nil, + true, + }, + + { + " =value", + nil, + true, + }, + + { + "key=value", + map[string]interface{}{"key": "value"}, + false, + }, + + { + "key=", + map[string]interface{}{"key": ""}, + false, + }, + + { + "key=foo=bar", + map[string]interface{}{"key": "foo=bar"}, + false, + }, + + { + "key=false", + map[string]interface{}{"key": "false"}, + false, + }, + + { + "key =value", + map[string]interface{}{"key": "value"}, + false, + }, + + { + "key = value", + map[string]interface{}{"key": " value"}, + false, + }, + + { + `key = "value"`, + map[string]interface{}{"key": "value"}, + false, + }, + + { + "map.key=foo", + map[string]interface{}{"map.key": "foo"}, + false, + }, + + { + "key", + nil, + true, + }, + + { + `key=["hello", "world"]`, + map[string]interface{}{"key": []interface{}{"hello", "world"}}, + false, + }, + + { + `key={"hello" = "world", "foo" = "bar"}`, + map[string]interface{}{ + "key": map[string]interface{}{ + "hello": "world", + "foo": "bar", + }, + }, + false, + }, + + { + `key={"hello" = "world", "foo" = "bar"}\nkey2="invalid"`, + nil, + true, + }, + + { + "key=/path", + map[string]interface{}{"key": "/path"}, + false, + }, + + { + "key=1234.dkr.ecr.us-east-1.amazonaws.com/proj:abcdef", + map[string]interface{}{"key": "1234.dkr.ecr.us-east-1.amazonaws.com/proj:abcdef"}, + false, + }, + + // simple values that can parse as numbers should remain strings + { + "key=1", + map[string]interface{}{ + "key": "1", + }, + false, + }, + { + "key=1.0", + map[string]interface{}{ + "key": "1.0", + }, + false, + }, + { + "key=0x10", + map[string]interface{}{ + "key": "0x10", + }, + false, + }, + + // Test setting multiple times + { + []string{ + "foo=bar", + "bar=baz", + }, + map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + false, + }, + + // Test map merging + { + []string{ + `foo={ foo = "bar" }`, + `foo={ bar = "baz" }`, + }, + map[string]interface{}{ + "foo": map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + }, + false, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%s", i, tc.Input), func(t *testing.T) { + var input []string + switch v := tc.Input.(type) { + case string: + input = []string{v} + case []string: + input = v + default: + t.Fatalf("bad input type: %T", tc.Input) + } + + f := new(FlagAny) + for i, single := range input { + err := f.Set(single) + + // Only check for expected errors on the final input + expected := tc.Error && i == len(input)-1 + if err != nil != expected { + t.Fatalf("bad error. Input: %#v\n\nError: %s", single, err) + } + } + + actual := map[string]interface{}(*f) + if !reflect.DeepEqual(actual, tc.Output) { + t.Fatalf("bad:\nexpected: %s\n\n got: %s\n", spew.Sdump(tc.Output), spew.Sdump(actual)) + } + }) + } +} + +func TestFlagAny_file(t *testing.T) { + inputLibucl := ` +foo = "bar" +` + inputMap := ` +foo = { + k = "v" +}` + + inputJson := `{ + "foo": "bar"}` + + cases := []struct { + Input interface{} + Output map[string]interface{} + Error bool + }{ + { + inputLibucl, + map[string]interface{}{"foo": "bar"}, + false, + }, + + { + inputJson, + map[string]interface{}{"foo": "bar"}, + false, + }, + + { + `map.key = "foo"`, + map[string]interface{}{"map.key": "foo"}, + false, + }, + + { + inputMap, + map[string]interface{}{ + "foo": map[string]interface{}{ + "k": "v", + }, + }, + false, + }, + + { + []string{ + `foo = { "k" = "v"}`, + `foo = { "j" = "v" }`, + }, + map[string]interface{}{ + "foo": map[string]interface{}{ + "k": "v", + "j": "v", + }, + }, + false, + }, + } + + path := testTempFile(t) + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var input []string + switch i := tc.Input.(type) { + case string: + input = []string{i} + case []string: + input = i + default: + t.Fatalf("bad input type: %T", i) + } + + f := new(FlagAny) + for _, input := range input { + if err := ioutil.WriteFile(path, []byte(input), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + err := f.Set(path) + if err != nil != tc.Error { + t.Fatalf("bad error. Input: %#v, err: %s", input, err) + } + } + + actual := map[string]interface{}(*f) + if !reflect.DeepEqual(actual, tc.Output) { + t.Fatalf("bad: %#v", actual) + } + }) + } +} diff --git a/website/source/docs/backends/config.html.md b/website/source/docs/backends/config.html.md index 9673f45167..47053e9529 100644 --- a/website/source/docs/backends/config.html.md +++ b/website/source/docs/backends/config.html.md @@ -54,7 +54,7 @@ the configuration itself. We call this specifying only a _partial_ configuration With a partial configuration, the remaining configuration is expected as part of the [initialization](/docs/backends/init.html) process. There are -two ways to supply the remaining configuration: +a few ways to supply the remaining configuration: * **Interactively**: Terraform will interactively ask you for the required values. Terraform will not ask you for optional values. @@ -63,7 +63,10 @@ two ways to supply the remaining configuration: This file can then be sourced via some secure means (such as [Vault](https://www.vaultproject.io)). -In both cases, the final configuration is stored on disk in the + * **Command-line key/value pairs**: Key/value pairs in the format of + `key=value` can be specified as part of the init command. + +In all cases, the final configuration is stored on disk in the ".terraform" directory, which should be ignored from version control. This means that sensitive information can be omitted from version control diff --git a/website/source/docs/commands/init.html.markdown b/website/source/docs/commands/init.html.markdown index 11047aaf82..6ec816fe4c 100644 --- a/website/source/docs/commands/init.html.markdown +++ b/website/source/docs/commands/init.html.markdown @@ -44,8 +44,10 @@ The command-line flags are all optional. The list of available flags are: * `-backend=true` - Initialize the [backend](/docs/backends) for this environment. -* `-backend-config=path` - Path to an HCL file with additional configuration - for the backend. This is merged with the backend in the Terraform configuration. +* `-backend-config=value` - Value can be a path to an HCL file or a string + in the format of 'key=value'. This specifies additional configuration to merge + for the backend. This can be specified multiple times. Flags specified + later in the line override those specified earlier if they conflict. * `-get=true` - Download any modules for this configuration. @@ -54,7 +56,7 @@ The command-line flags are all optional. The list of available flags are: ## Backend Config File -The `-backend-config` path can be used to specify additional +The `-backend-config` can take a path to specify additional backend configuration when [initialize a backend](/docs/backends/init.html). This is particularly useful for @@ -71,3 +73,28 @@ configuration file for the Consul backend type: address = "demo.consul.io" path = "newpath" ``` + +This format can be mixed with the key/value format documented below. In this +case, the values will be merged by key. + +## Backend Config Key/Value + +The `-backend-config` will also accept `key=value` pairs to specify configuration +directly on the command line. + +This is particularly useful for +[partial configuration of backends](/docs/backends/config.html). Partial +configuration lets you keep sensitive information out of your Terraform +configuration. + +The format of this flag is identical to the `-var` flag for plan, apply, +etc. but applies to configuration keys for backends. For example: + +``` +$ terraform init \ + -backend-config 'address=demo.consul.io' \ + -backend-config 'path=newpath' +``` + +This format can be mixed with the file format documented above. In this +case, the values will be merged by key.