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/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.
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue