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.
This commit is contained in:
parent
2082359220
commit
df8529719c
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/hashicorp/go-getter"
|
"github.com/hashicorp/go-getter"
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
"github.com/hashicorp/terraform/helper/variables"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitCommand is a Command implementation that takes a Terraform
|
// InitCommand is a Command implementation that takes a Terraform
|
||||||
|
@ -19,11 +20,11 @@ type InitCommand struct {
|
||||||
|
|
||||||
func (c *InitCommand) Run(args []string) int {
|
func (c *InitCommand) Run(args []string) int {
|
||||||
var flagBackend, flagGet bool
|
var flagBackend, flagGet bool
|
||||||
var flagConfigFile string
|
var flagConfigExtra map[string]interface{}
|
||||||
args = c.Meta.process(args, false)
|
args = c.Meta.process(args, false)
|
||||||
cmdFlags := c.flagSet("init")
|
cmdFlags := c.flagSet("init")
|
||||||
cmdFlags.BoolVar(&flagBackend, "backend", true, "")
|
cmdFlags.BoolVar(&flagBackend, "backend", true, "")
|
||||||
cmdFlags.StringVar(&flagConfigFile, "backend-config", "", "")
|
cmdFlags.Var((*variables.FlagAny)(&flagConfigExtra), "backend-config", "")
|
||||||
cmdFlags.BoolVar(&flagGet, "get", true, "")
|
cmdFlags.BoolVar(&flagGet, "get", true, "")
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
|
@ -138,9 +139,9 @@ func (c *InitCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &BackendOpts{
|
opts := &BackendOpts{
|
||||||
ConfigPath: path,
|
ConfigPath: path,
|
||||||
ConfigFile: flagConfigFile,
|
ConfigExtra: flagConfigExtra,
|
||||||
Init: true,
|
Init: true,
|
||||||
}
|
}
|
||||||
if _, err := c.Backend(opts); err != nil {
|
if _, err := c.Backend(opts); err != nil {
|
||||||
c.Ui.Error(err.Error())
|
c.Ui.Error(err.Error())
|
||||||
|
@ -210,8 +211,12 @@ Options:
|
||||||
|
|
||||||
-backend=true Configure the backend for this environment.
|
-backend=true Configure the backend for this environment.
|
||||||
|
|
||||||
-backend-config=path A path to load additional configuration for the backend.
|
-backend-config=path This can be either a path to an HCL file with key/value
|
||||||
This is merged with what is in the configuration file.
|
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.
|
-get=true Download any modules for this configuration.
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,10 @@ type BackendOpts struct {
|
||||||
// from a file.
|
// from a file.
|
||||||
ConfigFile string
|
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
|
// Plan is a plan that is being used. If this is set, the backend
|
||||||
// configuration and output configuration will come from this plan.
|
// configuration and output configuration will come from this plan.
|
||||||
Plan *terraform.Plan
|
Plan *terraform.Plan
|
||||||
|
@ -251,6 +255,20 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*config.Backend, error) {
|
||||||
backend.RawConfig = backend.RawConfig.Merge(rc)
|
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
|
// Validate the backend early. We have to do this before the normal
|
||||||
// config validation pass since backend loading happens earlier.
|
// config validation pass since backend loading happens earlier.
|
||||||
if errs := backend.Validate(); len(errs) > 0 {
|
if errs := backend.Validate(); len(errs) > 0 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
With a partial configuration, the remaining configuration is expected as
|
||||||
part of the [initialization](/docs/backends/init.html) process. There are
|
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
|
* **Interactively**: Terraform will interactively ask you for the required
|
||||||
values. Terraform will not ask you for optional values.
|
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
|
This file can then be sourced via some secure means (such as
|
||||||
[Vault](https://www.vaultproject.io)).
|
[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.
|
".terraform" directory, which should be ignored from version control.
|
||||||
|
|
||||||
This means that sensitive information can be omitted from version control
|
This means that sensitive information can be omitted from version control
|
||||||
|
|
|
@ -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=true` - Initialize the [backend](/docs/backends) for this environment.
|
||||||
|
|
||||||
* `-backend-config=path` - Path to an HCL file with additional configuration
|
* `-backend-config=value` - Value can be a path to an HCL file or a string
|
||||||
for the backend. This is merged with the backend in the Terraform configuration.
|
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.
|
* `-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
|
## 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).
|
backend configuration when [initialize a backend](/docs/backends/init.html).
|
||||||
|
|
||||||
This is particularly useful for
|
This is particularly useful for
|
||||||
|
@ -71,3 +73,28 @@ configuration file for the Consul backend type:
|
||||||
address = "demo.consul.io"
|
address = "demo.consul.io"
|
||||||
path = "newpath"
|
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.
|
||||||
|
|
Loading…
Reference in New Issue