diff --git a/backend/atlas/backend.go b/backend/atlas/backend.go index 655de23c9..cd839820d 100644 --- a/backend/atlas/backend.go +++ b/backend/atlas/backend.go @@ -1,14 +1,17 @@ package atlas import ( - "context" "fmt" "net/url" "os" "strings" "sync" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/config/configschema" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" @@ -17,6 +20,9 @@ import ( "github.com/mitchellh/colorstring" ) +const EnvVarToken = "ATLAS_TOKEN" +const EnvVarAddress = "ATLAS_ADDRESS" + // Backend is an implementation of EnhancedBackend that performs all operations // in Atlas. State must currently also be stored in Atlas, although it is worth // investigating in the future if state storage can be external as well. @@ -44,97 +50,130 @@ type Backend struct { opLock sync.Mutex } -// New returns a new initialized Atlas backend. -func New() *Backend { - b := &Backend{} - b.schema = &schema.Backend{ - Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, +func (b *Backend) ConfigSchema() *configschema.Block { + return &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, Required: true, - Description: schemaDescriptions["name"], + Description: "Full name of the environment in Terraform Enterprise, such as 'myorg/myenv'", }, - - "access_token": &schema.Schema{ - Type: schema.TypeString, - Required: true, - Description: schemaDescriptions["access_token"], - DefaultFunc: schema.EnvDefaultFunc("ATLAS_TOKEN", nil), - }, - - "address": &schema.Schema{ - Type: schema.TypeString, + "access_token": { + Type: cty.String, Optional: true, - Description: schemaDescriptions["address"], - DefaultFunc: schema.EnvDefaultFunc("ATLAS_ADDRESS", defaultAtlasServer), + Description: "Access token to use to access Terraform Enterprise; the ATLAS_TOKEN environment variable is used if this argument is not set", + }, + "address": { + Type: cty.String, + Optional: true, + Description: "Base URL for your Terraform Enterprise installation; the ATLAS_ADDRESS environment variable is used if this argument is not set, finally falling back to a default of 'https://atlas.hashicorp.com/' if neither are set.", }, }, - - ConfigureFunc: b.configure, } - - return b } -func (b *Backend) configure(ctx context.Context) error { - d := schema.FromContextBackendConfig(ctx) +func (b *Backend) ValidateConfig(obj cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics - // Parse the address - addr := d.Get("address").(string) - addrUrl, err := url.Parse(addr) - if err != nil { - return fmt.Errorf("Error parsing 'address': %s", err) + name := obj.GetAttr("name").AsString() + if ct := strings.Count(name, "/"); ct != 1 { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid workspace selector", + `The "name" argument must be an organization name and a workspace name separated by a slash, such as "acme/network-production".`, + cty.Path{cty.GetAttrStep{Name: "name"}}, + )) } - // Parse the org/env - name := d.Get("name").(string) - parts := strings.Split(name, "/") - if len(parts) != 2 { - return fmt.Errorf("malformed name '%s', expected format '/'", name) + if v := obj.GetAttr("address"); !v.IsNull() { + addr := v.AsString() + _, err := url.Parse(addr) + if err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid Terraform Enterprise URL", + fmt.Sprintf(`The "address" argument must be a valid URL: %s.`, err), + cty.Path{cty.GetAttrStep{Name: "address"}}, + )) + } } - org := parts[0] - env := parts[1] - // Setup the client - b.stateClient = &stateClient{ - Server: addr, - ServerURL: addrUrl, - AccessToken: d.Get("access_token").(string), - User: org, - Name: env, + return diags +} +func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + client := &stateClient{ // This is optionally set during Atlas Terraform runs. RunId: os.Getenv("ATLAS_RUN_ID"), } - return nil -} + name := obj.GetAttr("name").AsString() // assumed valid due to ValidateConfig method + slashIdx := strings.Index(name, "/") + client.User = name[:slashIdx] + client.Name = name[slashIdx+1:] -func (b *Backend) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - return b.schema.Input(ui, c) -} - -func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) { - return b.schema.Validate(c) -} - -func (b *Backend) Configure(c *terraform.ResourceConfig) error { - return b.schema.Configure(c) -} - -func (b *Backend) State(name string) (state.State, error) { - if name != backend.DefaultStateName { - return nil, backend.ErrNamedStatesNotSupported + if v := obj.GetAttr("access_token"); !v.IsNull() { + client.AccessToken = v.AsString() + } else { + client.AccessToken = os.Getenv(EnvVarToken) + if client.AccessToken == "" { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Missing Terraform Enterprise access token", + `The "access_token" argument must be set unless the ATLAS_TOKEN environment variable is set to provide the authentication token for Terraform Enterprise.`, + cty.Path{cty.GetAttrStep{Name: "access_token"}}, + )) + } } - return &remote.State{Client: b.stateClient}, nil + + if v := obj.GetAttr("address"); !v.IsNull() { + addr := v.AsString() + addrURL, err := url.Parse(addr) + if err != nil { + // We already validated the URL in ValidateConfig, so this shouldn't happen + panic(err) + } + client.Server = addr + client.ServerURL = addrURL + } else { + addr := os.Getenv(EnvVarAddress) + if addr == "" { + addr = defaultAtlasServer + } + addrURL, err := url.Parse(addr) + if err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid Terraform Enterprise URL", + fmt.Sprintf(`The ATLAS_ADDRESS environment variable must contain a valid URL: %s.`, err), + cty.Path{cty.GetAttrStep{Name: "address"}}, + )) + } + client.Server = addr + client.ServerURL = addrURL + } + + b.stateClient = client + + return diags +} + +func (b *Backend) States() ([]string, error) { + return nil, backend.ErrNamedStatesNotSupported } func (b *Backend) DeleteState(name string) error { return backend.ErrNamedStatesNotSupported } -func (b *Backend) States() ([]string, error) { - return nil, backend.ErrNamedStatesNotSupported +func (b *Backend) State(name string) (state.State, error) { + if name != backend.DefaultStateName { + return nil, backend.ErrNamedStatesNotSupported + } + + return &remote.State{Client: b.stateClient}, nil } // Colorize returns the Colorize structure that can be used for colorizing @@ -150,12 +189,3 @@ func (b *Backend) Colorize() *colorstring.Colorize { Disable: true, } } - -var schemaDescriptions = map[string]string{ - "name": "Full name of the environment in Atlas, such as 'hashicorp/myenv'", - "access_token": "Access token to use to access Atlas. If ATLAS_TOKEN is set then\n" + - "this will override any saved value for this.", - "address": "Address to your Atlas installation. This defaults to the publicly\n" + - "hosted version at 'https://atlas.hashicorp.com/'. This address\n" + - "should contain the full HTTP scheme to use.", -} diff --git a/backend/atlas/backend_test.go b/backend/atlas/backend_test.go index 286d3de8d..d42418865 100644 --- a/backend/atlas/backend_test.go +++ b/backend/atlas/backend_test.go @@ -4,9 +4,9 @@ import ( "os" "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/terraform" ) func TestImpl(t *testing.T) { @@ -18,16 +18,18 @@ func TestConfigure_envAddr(t *testing.T) { defer os.Setenv("ATLAS_ADDRESS", os.Getenv("ATLAS_ADDRESS")) os.Setenv("ATLAS_ADDRESS", "http://foo.com") - b := New() - err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{ - "name": "foo/bar", - }))) - if err != nil { - t.Fatalf("err: %s", err) + b := &Backend{} + diags := b.Configure(cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("foo/bar"), + "address": cty.NullVal(cty.String), + "access_token": cty.StringVal("placeholder"), + })) + for _, diag := range diags { + t.Error(diag) } - if b.stateClient.Server != "http://foo.com" { - t.Fatalf("bad: %#v", b.stateClient) + if got, want := b.stateClient.Server, "http://foo.com"; got != want { + t.Fatalf("wrong URL %#v; want %#v", got, want) } } @@ -35,15 +37,17 @@ func TestConfigure_envToken(t *testing.T) { defer os.Setenv("ATLAS_TOKEN", os.Getenv("ATLAS_TOKEN")) os.Setenv("ATLAS_TOKEN", "foo") - b := New() - err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{ - "name": "foo/bar", - }))) - if err != nil { - t.Fatalf("err: %s", err) + b := &Backend{} + diags := b.Configure(cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("foo/bar"), + "address": cty.NullVal(cty.String), + "access_token": cty.NullVal(cty.String), + })) + for _, diag := range diags { + t.Error(diag) } - if b.stateClient.AccessToken != "foo" { - t.Fatalf("bad: %#v", b.stateClient) + if got, want := b.stateClient.AccessToken, "foo"; got != want { + t.Fatalf("wrong access token %#v; want %#v", got, want) } } diff --git a/backend/atlas/state_client_test.go b/backend/atlas/state_client_test.go index 5135bfd7d..792b6b5c1 100644 --- a/backend/atlas/state_client_test.go +++ b/backend/atlas/state_client_test.go @@ -13,14 +13,23 @@ import ( "testing" "time" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/terraform" ) -func testStateClient(t *testing.T, c map[string]interface{}) remote.Client { - b := backend.TestBackendConfig(t, New(), c) +func testStateClient(t *testing.T, c map[string]string) remote.Client { + vals := make(map[string]cty.Value) + for k, s := range c { + vals[k] = cty.StringVal(s) + } + synthBody := configs.SynthBody("", vals) + + b := backend.TestBackendConfig(t, &Backend{}, synthBody) raw, err := b.State(backend.DefaultStateName) if err != nil { t.Fatalf("err: %s", err) @@ -42,7 +51,7 @@ func TestStateClient(t *testing.T) { t.Skipf("skipping, ATLAS_TOKEN must be set") } - client := testStateClient(t, map[string]interface{}{ + client := testStateClient(t, map[string]string{ "access_token": token, "name": "hashicorp/test-remote-state", }) @@ -53,7 +62,7 @@ func TestStateClient(t *testing.T) { func TestStateClient_noRetryOnBadCerts(t *testing.T) { acctest.RemoteTestPrecheck(t) - client := testStateClient(t, map[string]interface{}{ + client := testStateClient(t, map[string]string{ "access_token": "NOT_REQUIRED", "name": "hashicorp/test-remote-state", }) @@ -99,7 +108,7 @@ func TestStateClient_ReportedConflictEqualStates(t *testing.T) { srv := fakeAtlas.Server() defer srv.Close() - client := testStateClient(t, map[string]interface{}{ + client := testStateClient(t, map[string]string{ "access_token": "sometoken", "name": "someuser/some-test-remote-state", "address": srv.URL, @@ -124,7 +133,7 @@ func TestStateClient_NoConflict(t *testing.T) { srv := fakeAtlas.Server() defer srv.Close() - client := testStateClient(t, map[string]interface{}{ + client := testStateClient(t, map[string]string{ "access_token": "sometoken", "name": "someuser/some-test-remote-state", "address": srv.URL, @@ -152,7 +161,7 @@ func TestStateClient_LegitimateConflict(t *testing.T) { srv := fakeAtlas.Server() defer srv.Close() - client := testStateClient(t, map[string]interface{}{ + client := testStateClient(t, map[string]string{ "access_token": "sometoken", "name": "someuser/some-test-remote-state", "address": srv.URL, @@ -191,7 +200,7 @@ func TestStateClient_UnresolvableConflict(t *testing.T) { srv := fakeAtlas.Server() defer srv.Close() - client := testStateClient(t, map[string]interface{}{ + client := testStateClient(t, map[string]string{ "access_token": "sometoken", "name": "someuser/some-test-remote-state", "address": srv.URL, diff --git a/backend/backend.go b/backend/backend.go index 840cb06ac..414f60c81 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -9,10 +9,16 @@ import ( "errors" "time" + "github.com/hashicorp/terraform/configs" + + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/command/clistate" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/config/configschema" + "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) // DefaultStateName is the name of the default, initial state that every @@ -41,11 +47,45 @@ type InitFn func() Backend // Backend is the minimal interface that must be implemented to enable Terraform. type Backend interface { - // Ask for input and configure the backend. Similar to - // terraform.ResourceProvider. - Input(terraform.UIInput, *terraform.ResourceConfig) (*terraform.ResourceConfig, error) - Validate(*terraform.ResourceConfig) ([]string, []error) - Configure(*terraform.ResourceConfig) error + // ConfigSchema returns a description of the expected configuration + // structure for the receiving backend. + // + // This method does not have any side-effects for the backend and can + // be safely used before configuring. + ConfigSchema() *configschema.Block + + // ValidateConfig checks the validity of the values in the given + // configuration, assuming that its structure has already been validated + // per the schema returned by ConfigSchema. + // + // This method does not have any side-effects for the backend and can + // be safely used before configuring. It also does not consult any + // external data such as environment variables, disk files, etc. Validation + // that requires such external data should be deferred until the + // Configure call. + // + // If error diagnostics are returned then the configuration is not valid + // and must not subsequently be passed to the Configure method. + // + // This method may return configuration-contextual diagnostics such + // as tfdiags.AttributeValue, and so the caller should provide the + // necessary context via the diags.InConfigBody method before returning + // diagnostics to the user. + ValidateConfig(cty.Value) tfdiags.Diagnostics + + // Configure uses the provided configuration to set configuration fields + // within the backend. + // + // The given configuration is assumed to have already been validated + // against the schema returned by ConfigSchema and passed validation + // via ValidateConfig. + // + // This method may be called only once per backend instance, and must be + // called before all other methods except where otherwise stated. + // + // If error diagnostics are returned, the internal state of the instance + // is undefined and no other methods may be called. + Configure(cty.Value) tfdiags.Diagnostics // State returns the current state for this environment. This state may // not be loaded locally: the proper APIs should be called on state.State @@ -96,7 +136,7 @@ type Enhanced interface { type Local interface { // Context returns a runnable terraform Context. The operation parameter // doesn't need a Type set but it needs other options set such as Module. - Context(*Operation) (*terraform.Context, state.State, error) + Context(*Operation) (*terraform.Context, state.State, tfdiags.Diagnostics) } // An operation represents an operation for Terraform to execute. @@ -128,8 +168,13 @@ type Operation struct { PlanOutPath string // PlanOutPath is the path to save the plan PlanOutBackend *terraform.BackendState - // Module settings specify the root module to use for operations. - Module *module.Tree + // ConfigDir is the path to the directory containing the configuration's + // root module. + ConfigDir string + + // ConfigLoader is a configuration loader that can be used to load + // configuration from ConfigDir. + ConfigLoader *configload.Loader // Plan is a plan that was passed as an argument. This is valid for // plan and apply arguments but may not work for all backends. @@ -165,6 +210,22 @@ type Operation struct { Workspace string } +// HasConfig returns true if and only if the operation has a ConfigDir value +// that refers to a directory containing at least one Terraform configuration +// file. +func (o *Operation) HasConfig() bool { + return o.ConfigLoader.IsConfigDir(o.ConfigDir) +} + +// Config loads the configuration that the operation applies to, using the +// ConfigDir and ConfigLoader fields within the receiving operation. +func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir) + diags = diags.Append(hclDiags) + return config, diags +} + // RunningOperation is the result of starting an operation. type RunningOperation struct { // For implementers of a backend, this context should not wrap the @@ -184,9 +245,9 @@ type RunningOperation struct { // to avoid running operations during process exit. Cancel context.CancelFunc - // Err is the error of the operation. This is populated after - // the operation has completed. - Err error + // Result is the exit status of the operation, populated only after the + // operation has completed. + Result OperationResult // ExitCode can be used to set a custom exit code. This enables enhanced // backends to set specific exit codes that miror any remote exit codes. @@ -201,3 +262,16 @@ type RunningOperation struct { // after the operation completes to avoid read/write races. State *terraform.State } + +// OperationResult describes the result status of an operation. +type OperationResult int + +const ( + // OperationSuccess indicates that the operation completed as expected. + OperationSuccess OperationResult = 0 + + // OperationFailure indicates that the operation encountered some sort + // of error, and thus may have been only partially performed or not + // performed at all. + OperationFailure OperationResult = 1 +) diff --git a/backend/cli.go b/backend/cli.go index b76b6e111..cd29e3862 100644 --- a/backend/cli.go +++ b/backend/cli.go @@ -48,6 +48,10 @@ type CLIOpts struct { CLI cli.Ui CLIColor *colorstring.Colorize + // ShowDiagnostics is a function that will format and print diagnostic + // messages to the UI. + ShowDiagnostics func(vals ...interface{}) + // StatePath is the local path where state is read from. // // StateOutPath is the local path where the state will be written. diff --git a/backend/init/deprecate_test.go b/backend/init/deprecate_test.go index 3c968151f..61d252a0d 100644 --- a/backend/init/deprecate_test.go +++ b/backend/init/deprecate_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/hashicorp/terraform/backend/remote-state/inmem" - "github.com/hashicorp/terraform/terraform" + "github.com/zclconf/go-cty/cty" ) func TestDeprecateBackend(t *testing.T) { @@ -12,21 +12,19 @@ func TestDeprecateBackend(t *testing.T) { deprecatedBackend := deprecateBackend( inmem.New(), deprecateMessage, - )() + ) - warns, errs := deprecatedBackend.Validate(&terraform.ResourceConfig{}) - if errs != nil { - for _, err := range errs { - t.Error(err) + diags := deprecatedBackend.ValidateConfig(cty.EmptyObjectVal) + if len(diags) != 1 { + t.Errorf("got %d diagnostics; want 1", len(diags)) + for _, diag := range diags { + t.Errorf("- %s", diag) } - t.Fatal("validation errors") + return } - if len(warns) != 1 { - t.Fatalf("expected 1 warning, got %q", warns) - } - - if warns[0] != deprecateMessage { - t.Fatalf("expected %q, got %q", deprecateMessage, warns[0]) + desc := diags[0].Description() + if desc.Summary != deprecateMessage { + t.Fatalf("wrong message %q; want %q", desc.Summary, deprecateMessage) } } diff --git a/backend/local/backend.go b/backend/local/backend.go index d89748437..57ab0ae40 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -12,13 +12,17 @@ import ( "strings" "sync" + "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/config/configschema" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "github.com/zclconf/go-cty/cty" ) const ( @@ -37,6 +41,9 @@ type Local struct { CLI cli.Ui CLIColor *colorstring.Colorize + // ShowDiagnostics prints diagnostic messages to the UI. + ShowDiagnostics func(vals ...interface{}) + // The State* paths are set from the backend config, and may be left blank // to use the defaults. If the actual paths for the local backend state are // needed, use the StatePaths method. @@ -89,106 +96,92 @@ type Local struct { // exact commands that are being run. RunningInAutomation bool - schema *schema.Backend opLock sync.Mutex } -// New returns a new initialized local backend. -func New() *Local { - return NewWithBackend(nil) -} - -// NewWithBackend returns a new local backend initialized with a -// dedicated backend for non-enhanced behavior. -func NewWithBackend(backend backend.Backend) *Local { - b := &Local{ - Backend: backend, +func (b *Local) ConfigSchema() *configschema.Block { + if b.Backend != nil { + return b.Backend.ConfigSchema() } - - b.schema = &schema.Backend{ - Schema: map[string]*schema.Schema{ - "path": &schema.Schema{ - Type: schema.TypeString, + return &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "path": { + Type: cty.String, Optional: true, - Default: "", }, - - "workspace_dir": &schema.Schema{ - Type: schema.TypeString, + "workspace_dir": { + Type: cty.String, Optional: true, - Default: "", - }, - - "environment_dir": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "", - ConflictsWith: []string{"workspace_dir"}, - Deprecated: "workspace_dir should be used instead, with the same meaning", }, + // environment_dir was previously a deprecated alias for + // workspace_dir, but now removed. }, - - ConfigureFunc: b.configure, } - - return b } -func (b *Local) configure(ctx context.Context) error { - d := schema.FromContextBackendConfig(ctx) - - // Set the path if it is set - pathRaw, ok := d.GetOk("path") - if ok { - path := pathRaw.(string) - if path == "" { - return fmt.Errorf("configured path is empty") - } - - b.StatePath = path - b.StateOutPath = path +func (b *Local) ValidateConfig(obj cty.Value) tfdiags.Diagnostics { + if b.Backend != nil { + return b.Backend.ValidateConfig(obj) } - if raw, ok := d.GetOk("workspace_dir"); ok { - path := raw.(string) - if path != "" { - b.StateWorkspaceDir = path + var diags tfdiags.Diagnostics + + if val := obj.GetAttr("path"); !val.IsNull() { + p := val.AsString() + if p == "" { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid local state file path", + `The "path" attribute value must not be empty.`, + cty.Path{cty.GetAttrStep{Name: "path"}}, + )) } } - // Legacy name, which ConflictsWith workspace_dir - if raw, ok := d.GetOk("environment_dir"); ok { - path := raw.(string) - if path != "" { - b.StateWorkspaceDir = path + if val := obj.GetAttr("workspace_dir"); !val.IsNull() { + p := val.AsString() + if p == "" { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid local workspace directory path", + `The "workspace_dir" attribute value must not be empty.`, + cty.Path{cty.GetAttrStep{Name: "workspace_dir"}}, + )) } } - return nil + return diags } -func (b *Local) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - f := b.schema.Input +func (b *Local) Configure(obj cty.Value) tfdiags.Diagnostics { if b.Backend != nil { - f = b.Backend.Input + return b.Backend.Configure(obj) } - return f(ui, c) -} -func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) { - f := b.schema.Validate - if b.Backend != nil { - f = b.Backend.Validate - } - return f(c) -} + var diags tfdiags.Diagnostics -func (b *Local) Configure(c *terraform.ResourceConfig) error { - f := b.schema.Configure - if b.Backend != nil { - f = b.Backend.Configure + type Config struct { + Path string `hcl:"path,optional"` + WorkspaceDir string `hcl:"workspace_dir,optional"` } - return f(c) + + if val := obj.GetAttr("path"); !val.IsNull() { + p := val.AsString() + b.StatePath = p + b.StateOutPath = p + } else { + b.StatePath = DefaultStateFilename + b.StateOutPath = DefaultStateFilename + } + + if val := obj.GetAttr("workspace_dir"); !val.IsNull() { + p := val.AsString() + b.StateWorkspaceDir = p + } else { + b.StateWorkspaceDir = DefaultWorkspaceDir + } + + return diags } func (b *Local) State(name string) (state.State, error) { @@ -339,7 +332,11 @@ func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend. // the state was locked during context creation, unlock the state when // the operation completes defer func() { - runningOp.Err = op.StateLocker.Unlock(runningOp.Err) + err := op.StateLocker.Unlock(nil) + if err != nil { + b.ShowDiagnostics(err) + } + runningOp.Result = backend.OperationFailure }() defer b.opLock.Unlock() @@ -397,6 +394,28 @@ func (b *Local) opWait( return } +// ReportResult is a helper for the common chore of setting the status of +// a running operation and showing any diagnostics produced during that +// operation. +// +// If the given diagnostics contains errors then the operation's result +// will be set to backend.OperationFailure. It will be set to +// backend.OperationSuccess otherwise. It will then use b.ShowDiagnostics +// to show the given diagnostics before returning. +// +// Callers should feel free to do each of these operations separately in +// more complex cases where e.g. diagnostics are interleaved with other +// output, but terminating immediately after reporting error diagnostics is +// common and can be expressed concisely via this method. +func (b *Local) ReportResult(op *backend.RunningOperation, diags tfdiags.Diagnostics) { + if diags.HasErrors() { + op.Result = backend.OperationFailure + } else { + op.Result = backend.OperationSuccess + } + b.ShowDiagnostics(diags) +} + // Colorize returns the Colorize structure that can be used for colorizing // output. This is gauranteed to always return a non-nil value and so is useful // as a helper to wrap any potentially colored strings. @@ -411,6 +430,39 @@ func (b *Local) Colorize() *colorstring.Colorize { } } +func (b *Local) schemaConfigure(ctx context.Context) error { + d := schema.FromContextBackendConfig(ctx) + + // Set the path if it is set + pathRaw, ok := d.GetOk("path") + if ok { + path := pathRaw.(string) + if path == "" { + return fmt.Errorf("configured path is empty") + } + + b.StatePath = path + b.StateOutPath = path + } + + if raw, ok := d.GetOk("workspace_dir"); ok { + path := raw.(string) + if path != "" { + b.StateWorkspaceDir = path + } + } + + // Legacy name, which ConflictsWith workspace_dir + if raw, ok := d.GetOk("environment_dir"); ok { + path := raw.(string) + if path != "" { + b.StateWorkspaceDir = path + } + } + + return nil +} + // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as // configured from the CLI. func (b *Local) StatePaths(name string) (string, string, string) { diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index 12a9385c6..fef67b8de 100644 --- a/backend/local/backend_apply.go +++ b/backend/local/backend_apply.go @@ -6,15 +6,13 @@ import ( "errors" "fmt" "log" - "strings" "github.com/hashicorp/errwrap" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/format" - "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) func (b *Local) opApply( @@ -24,17 +22,18 @@ func (b *Local) opApply( runningOp *backend.RunningOperation) { log.Printf("[INFO] backend/local: starting Apply operation") - // If we have a nil module at this point, then set it to an empty tree - // to avoid any potential crashes. - if op.Plan == nil && op.Module == nil && !op.Destroy { - runningOp.Err = fmt.Errorf(strings.TrimSpace(applyErrNoConfig)) - return - } + var diags tfdiags.Diagnostics // If we have a nil module at this point, then set it to an empty tree // to avoid any potential crashes. - if op.Module == nil { - op.Module = module.NewEmptyTree() + if op.Plan == nil && !op.Destroy && !op.HasConfig() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No configuration files", + "Apply requires configuration to be present. Applying without a configuration would mark everything for destruction, which is normally not what is desired. If you would like to destroy everything, run 'terraform destroy' instead.", + )) + b.ReportResult(runningOp, diags) + return } // Setup our count hook that keeps track of resource changes @@ -50,7 +49,8 @@ func (b *Local) opApply( // Get our context tfCtx, opState, err := b.context(op) if err != nil { - runningOp.Err = err + diags = diags.Append(err) + b.ReportResult(runningOp, diags) return } @@ -64,7 +64,9 @@ func (b *Local) opApply( log.Printf("[INFO] backend/local: apply calling Refresh") _, err := tfCtx.Refresh() if err != nil { - runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err) + diags = diags.Append(err) + runningOp.Result = backend.OperationFailure + b.ShowDiagnostics(diags) return } } @@ -73,7 +75,8 @@ func (b *Local) opApply( log.Printf("[INFO] backend/local: apply calling Plan") plan, err := tfCtx.Plan() if err != nil { - runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", err) + diags = diags.Append(err) + b.ReportResult(runningOp, diags) return } @@ -113,15 +116,17 @@ func (b *Local) opApply( Description: desc, }) if err != nil { - runningOp.Err = errwrap.Wrapf("Error asking for approval: {{err}}", err) + diags = diags.Append(errwrap.Wrapf("Error asking for approval: {{err}}", err)) + b.ReportResult(runningOp, diags) return } if v != "yes" { if op.Destroy { - runningOp.Err = errors.New("Destroy cancelled.") + b.CLI.Info("Destroy cancelled.") } else { - runningOp.Err = errors.New("Apply cancelled.") + b.CLI.Info("Apply cancelled.") } + runningOp.Result = backend.OperationFailure return } } @@ -150,26 +155,31 @@ func (b *Local) opApply( // Persist the state if err := opState.WriteState(applyState); err != nil { - runningOp.Err = b.backupStateForError(applyState, err) + diags = diags.Append(b.backupStateForError(applyState, err)) + b.ReportResult(runningOp, diags) return } if err := opState.PersistState(); err != nil { - runningOp.Err = b.backupStateForError(applyState, err) + diags = diags.Append(b.backupStateForError(applyState, err)) + b.ReportResult(runningOp, diags) return } if applyErr != nil { - runningOp.Err = fmt.Errorf( - "Error applying plan:\n\n"+ - "%s\n\n"+ - "Terraform does not automatically rollback in the face of errors.\n"+ - "Instead, your Terraform state file has been partially updated with\n"+ - "any resources that successfully completed. Please address the error\n"+ - "above and apply again to incrementally change your infrastructure.", - multierror.Flatten(applyErr)) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + applyErr.Error(), + "Terraform does not automatically rollback in the face of errors. Instead, your Terraform state file has been partially updated with any resources that successfully completed. Please address the error above and apply again to incrementally change your infrastructure.", + )) + b.ReportResult(runningOp, diags) return } + // If we've accumulated any warnings along the way then we'll show them + // here just before we show the summary and next steps. If we encountered + // errors then we would've returned early at some other point above. + b.ShowDiagnostics(diags) + // If we have a UI, output the results if b.CLI != nil { if op.Destroy { @@ -236,15 +246,6 @@ func (b *Local) backupStateForError(applyState *terraform.State, err error) erro return errors.New(stateWriteBackedUpError) } -const applyErrNoConfig = ` -No configuration files found! - -Apply requires configuration to be present. Applying without a configuration -would mark everything for destruction, which is normally not what is desired. -If you would like to destroy everything, please run 'terraform destroy' which -does not require any configuration files. -` - const stateWriteBackedUpError = `Failed to persist state to backend. The error shown above has prevented Terraform from writing the updated state diff --git a/backend/local/backend_apply_test.go b/backend/local/backend_apply_test.go index 19cc1140a..ebf1d0785 100644 --- a/backend/local/backend_apply_test.go +++ b/backend/local/backend_apply_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" @@ -24,19 +24,16 @@ func TestLocal_applyBasic(t *testing.T) { p.ApplyReturn = &terraform.InstanceState{ID: "yes"} - mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") - defer modCleanup() - - op := testOperationApply() - op.Module = mod + op, configCleanup := testOperationApply(t, "./test-fixtures/apply") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("err: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatal("operation failed") } if p.RefreshCalled { @@ -66,16 +63,16 @@ func TestLocal_applyEmptyDir(t *testing.T) { p.ApplyReturn = &terraform.InstanceState{ID: "yes"} - op := testOperationApply() - op.Module = nil + op, configCleanup := testOperationApply(t, "./test-fixtures/empty") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err == nil { - t.Fatal("should error") + if run.Result == backend.OperationSuccess { + t.Fatal("operation succeeded; want error") } if p.ApplyCalled { @@ -94,8 +91,8 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) { p.ApplyReturn = nil - op := testOperationApply() - op.Module = nil + op, configCleanup := testOperationApply(t, "./test-fixtures/empty") + defer configCleanup() op.Destroy = true run, err := b.Operation(context.Background(), op) @@ -103,8 +100,8 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("err: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("apply operation failed") } if p.ApplyCalled { @@ -148,19 +145,16 @@ func TestLocal_applyError(t *testing.T) { }, nil } - mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-error") - defer modCleanup() - - op := testOperationApply() - op.Module = mod + op, configCleanup := testOperationApply(t, "./test-fixtures/apply-error") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err == nil { - t.Fatal("should error") + if run.Result == backend.OperationSuccess { + t.Fatal("operation succeeded; want failure") } checkState(t, b.StateOutPath, ` @@ -171,8 +165,8 @@ test_instance.foo: } func TestLocal_applyBackendFail(t *testing.T) { - mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") - defer modCleanup() + op, configCleanup := testOperationApply(t, "./test-fixtures/apply") + defer configCleanup() b, cleanup := TestLocal(t) defer cleanup() @@ -193,23 +187,15 @@ func TestLocal_applyBackendFail(t *testing.T) { p.ApplyReturn = &terraform.InstanceState{ID: "yes"} - op := testOperationApply() - op.Module = mod - run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err == nil { + if run.Result == backend.OperationSuccess { t.Fatalf("apply succeeded; want error") } - errStr := run.Err.Error() - if !strings.Contains(errStr, "terraform state push errored.tfstate") { - t.Fatalf("wrong error message:\n%s", errStr) - } - msgStr := b.CLI.(*cli.MockUi).ErrorWriter.String() if !strings.Contains(msgStr, "Failed to save state: fake failure") { t.Fatalf("missing original error message in output:\n%s", msgStr) @@ -244,10 +230,16 @@ func (s failingState) WriteState(state *terraform.State) error { return errors.New("fake failure") } -func testOperationApply() *backend.Operation { +func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func()) { + t.Helper() + + _, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir) + return &backend.Operation{ - Type: backend.OperationTypeApply, - } + Type: backend.OperationTypeApply, + ConfigDir: configDir, + ConfigLoader: configLoader, + }, configCleanup } // testApplyState is just a common state that we use for testing refresh. diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 9aaf1c486..d6012fdd9 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -3,19 +3,18 @@ package local import ( "context" "errors" - "log" "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/clistate" - "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) // backend.Local implementation. -func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, error) { +func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, tfdiags.Diagnostics) { // Make sure the type is invalid. We use this as a way to know not // to ask for input/validate. op.Type = backend.OperationTypeInvalid @@ -29,19 +28,24 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State, return b.context(op) } -func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) { +func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + // Get the state. s, err := b.State(op.Workspace) if err != nil { - return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err) + diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) + return nil, nil, diags } if err := op.StateLocker.Lock(s, op.Type.String()); err != nil { - return nil, nil, errwrap.Wrapf("Error locking state: {{err}}", err) + diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err)) + return nil, nil, diags } if err := s.RefreshState(); err != nil { - return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err) + diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) + return nil, nil, diags } // Initialize our context options @@ -52,13 +56,18 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, // Copy set options from the operation opts.Destroy = op.Destroy - opts.Module = op.Module opts.Targets = op.Targets opts.UIInput = op.UIIn if op.Variables != nil { opts.Variables = op.Variables } + // FIXME: Configuration is temporarily stubbed out here to artificially + // create a stopping point in our work to switch to the new config loader. + // This means no backend-provided Terraform operations will actually work. + // This will be addressed in a subsequent commit. + opts.Module = nil + // Load our state // By the time we get here, the backend creation code in "command" took // care of making s.State() return a state compatible with our plan, @@ -79,11 +88,13 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, b.pluginInitRequired(rpe) // we wrote the full UI error here, so return a generic error for flow // control in the command. - return nil, nil, errors.New("error satisfying plugin requirements") + diags = diags.Append(errors.New("Can't satisfy plugin requirements")) + return nil, nil, diags } if err != nil { - return nil, nil, err + diags = diags.Append(err) + return nil, nil, diags } // If we have an operation, then we automatically do the input/validate @@ -96,45 +107,19 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, mode |= terraform.InputModeVarUnset if err := tfCtx.Input(mode); err != nil { - return nil, nil, errwrap.Wrapf("Error asking for user input: {{err}}", err) + diags = diags.Append(errwrap.Wrapf("Error asking for user input: {{err}}", err)) + return nil, nil, diags } } // If validation is enabled, validate if b.OpValidation { - diags := tfCtx.Validate() - if len(diags) > 0 { - if diags.HasErrors() { - // If there are warnings _and_ errors then we'll take this - // path and return them all together in this error. - return nil, nil, diags.Err() - } - - // For now we can't propagate warnings any further without - // printing them directly to the UI, so we'll need to - // format them here ourselves. - for _, diag := range diags { - if diag.Severity() != tfdiags.Warning { - continue - } - if b.CLI != nil { - // FIXME: We don't have access to the source code cache - // in here, so we can't produce source code snippets - // from this codepath. - b.CLI.Warn(format.Diagnostic(diag, nil, b.Colorize(), 72)) - } else { - desc := diag.Description() - log.Printf("[WARN] backend/local: %s", desc.Summary) - } - } - - // Make a newline before continuing - b.CLI.Output("") - } + validateDiags := tfCtx.Validate() + diags = diags.Append(validateDiags) } } - return tfCtx, s, nil + return tfCtx, s, diags } const validateWarnHeader = ` diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 00c0a859d..762294107 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -8,10 +8,10 @@ import ( "os" "strings" - "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/tfdiags" + "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/format" - "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" ) @@ -20,27 +20,30 @@ func (b *Local) opPlan( cancelCtx context.Context, op *backend.Operation, runningOp *backend.RunningOperation) { + log.Printf("[INFO] backend/local: starting Plan operation") - if b.CLI != nil && op.Plan != nil { - b.CLI.Output(b.Colorize().Color( - "[reset][bold][yellow]" + - "The plan command received a saved plan file as input. This command\n" + - "will output the saved plan. This will not modify the already-existing\n" + - "plan. If you wish to generate a new plan, please pass in a configuration\n" + - "directory as an argument.\n\n")) - } + var diags tfdiags.Diagnostics - // A local plan requires either a plan or a module - if op.Plan == nil && op.Module == nil && !op.Destroy { - runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrNoConfig)) + if b.CLI != nil && op.Plan != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Can't re-plan a saved plan", + "The plan command was given a saved plan file as its input. This command generates a new plan, and so it requires a configuration directory as its argument.", + )) + b.ReportResult(runningOp, diags) return } - // If we have a nil module at this point, then set it to an empty tree - // to avoid any potential crashes. - if op.Module == nil { - op.Module = module.NewEmptyTree() + // Local planning requires a config, unless we're planning to destroy. + if !op.Destroy && !op.HasConfig() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No configuration files", + "Plan requires configuration to be present. Planning without a configuration would mark everything for destruction, which is normally not what is desired. If you would like to destroy everything, run plan with the -destroy option. Otherwise, create a Terraform configuration file (.tf file) and try again.", + )) + b.ReportResult(runningOp, diags) + return } // Setup our count hook that keeps track of resource changes @@ -55,7 +58,8 @@ func (b *Local) opPlan( // Get our context tfCtx, opState, err := b.context(op) if err != nil { - runningOp.Err = err + diags = diags.Append(err) + b.ReportResult(runningOp, diags) return } @@ -72,7 +76,8 @@ func (b *Local) opPlan( _, err := tfCtx.Refresh() if err != nil { - runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err) + diags = diags.Append(err) + b.ReportResult(runningOp, diags) return } if b.CLI != nil { @@ -95,7 +100,8 @@ func (b *Local) opPlan( } if planErr != nil { - runningOp.Err = errwrap.Wrapf("Error running plan: {{err}}", planErr) + diags = diags.Append(planErr) + b.ReportResult(runningOp, diags) return } // Record state @@ -119,7 +125,12 @@ func (b *Local) opPlan( } f.Close() if err != nil { - runningOp.Err = fmt.Errorf("Error writing plan file: %s", err) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to write plan file", + fmt.Sprintf("The plan file could not be written: %s.", err), + )) + b.ReportResult(runningOp, diags) return } } @@ -134,6 +145,11 @@ func (b *Local) opPlan( b.renderPlan(dispPlan) + // If we've accumulated any warnings along the way then we'll show them + // here just before we show the summary and next steps. If we encountered + // errors then we would've returned early at some other point above. + b.ShowDiagnostics(diags) + // Give the user some next-steps, unless we're running in an automation // tool which is presumed to provide its own UI for further actions. if !b.RunningInAutomation { diff --git a/backend/local/backend_plan_test.go b/backend/local/backend_plan_test.go index 33832a9ec..8c92d0d37 100644 --- a/backend/local/backend_plan_test.go +++ b/backend/local/backend_plan_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" ) @@ -19,11 +19,8 @@ func TestLocal_planBasic(t *testing.T) { defer cleanup() p := TestLocalProvider(t, b, "test") - mod, modCleanup := module.TestTree(t, "./test-fixtures/plan") - defer modCleanup() - - op := testOperationPlan() - op.Module = mod + op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") + defer configCleanup() op.PlanRefresh = true run, err := b.Operation(context.Background(), op) @@ -31,8 +28,8 @@ func TestLocal_planBasic(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("err: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("plan operation failed") } if !p.DiffCalled { @@ -45,9 +42,6 @@ func TestLocal_planInAutomation(t *testing.T) { defer cleanup() TestLocalProvider(t, b, "test") - mod, modCleanup := module.TestTree(t, "./test-fixtures/plan") - defer modCleanup() - const msg = `You didn't specify an "-out" parameter` // When we're "in automation" we omit certain text from the @@ -59,8 +53,8 @@ func TestLocal_planInAutomation(t *testing.T) { b.RunningInAutomation = false b.CLI = cli.NewMockUi() { - op := testOperationPlan() - op.Module = mod + op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") + defer configCleanup() op.PlanRefresh = true run, err := b.Operation(context.Background(), op) @@ -68,8 +62,8 @@ func TestLocal_planInAutomation(t *testing.T) { t.Fatalf("unexpected error: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("unexpected error: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("plan operation failed") } output := b.CLI.(*cli.MockUi).OutputWriter.String() @@ -83,8 +77,8 @@ func TestLocal_planInAutomation(t *testing.T) { b.RunningInAutomation = true b.CLI = cli.NewMockUi() { - op := testOperationPlan() - op.Module = mod + op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") + defer configCleanup() op.PlanRefresh = true run, err := b.Operation(context.Background(), op) @@ -92,8 +86,8 @@ func TestLocal_planInAutomation(t *testing.T) { t.Fatalf("unexpected error: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("unexpected error: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("plan operation failed") } output := b.CLI.(*cli.MockUi).OutputWriter.String() @@ -109,8 +103,10 @@ func TestLocal_planNoConfig(t *testing.T) { defer cleanup() TestLocalProvider(t, b, "test") - op := testOperationPlan() - op.Module = nil + b.CLI = cli.NewMockUi() + + op, configCleanup := testOperationPlan(t, "./test-fixtures/empty") + defer configCleanup() op.PlanRefresh = true run, err := b.Operation(context.Background(), op) @@ -119,11 +115,11 @@ func TestLocal_planNoConfig(t *testing.T) { } <-run.Done() - err = run.Err - if err == nil { - t.Fatal("should error") + if run.Result == backend.OperationSuccess { + t.Fatal("plan operation succeeded; want failure") } - if !strings.Contains(err.Error(), "configuration") { + output := b.CLI.(*cli.MockUi).ErrorWriter.String() + if !strings.Contains(output, "configuration") { t.Fatalf("bad: %s", err) } } @@ -134,19 +130,16 @@ func TestLocal_planRefreshFalse(t *testing.T) { p := TestLocalProvider(t, b, "test") terraform.TestStateFile(t, b.StatePath, testPlanState()) - mod, modCleanup := module.TestTree(t, "./test-fixtures/plan") - defer modCleanup() - - op := testOperationPlan() - op.Module = mod + op, configCleanup := testOperationPlan(t, "./test-fixtures/empty") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("err: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("plan operation failed") } if p.RefreshCalled { @@ -164,17 +157,14 @@ func TestLocal_planDestroy(t *testing.T) { p := TestLocalProvider(t, b, "test") terraform.TestStateFile(t, b.StatePath, testPlanState()) - mod, modCleanup := module.TestTree(t, "./test-fixtures/plan") - defer modCleanup() - outDir := testTempDir(t) defer os.RemoveAll(outDir) planPath := filepath.Join(outDir, "plan.tfplan") - op := testOperationPlan() + op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") + defer configCleanup() op.Destroy = true op.PlanRefresh = true - op.Module = mod op.PlanOutPath = planPath run, err := b.Operation(context.Background(), op) @@ -182,8 +172,8 @@ func TestLocal_planDestroy(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("err: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("plan operation failed") } if !p.RefreshCalled { @@ -210,15 +200,12 @@ func TestLocal_planOutPathNoChange(t *testing.T) { TestLocalProvider(t, b, "test") terraform.TestStateFile(t, b.StatePath, testPlanState()) - mod, modCleanup := module.TestTree(t, "./test-fixtures/plan") - defer modCleanup() - outDir := testTempDir(t) defer os.RemoveAll(outDir) planPath := filepath.Join(outDir, "plan.tfplan") - op := testOperationPlan() - op.Module = mod + op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") + defer configCleanup() op.PlanOutPath = planPath run, err := b.Operation(context.Background(), op) @@ -226,8 +213,8 @@ func TestLocal_planOutPathNoChange(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("err: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("plan operation failed") } plan := testReadPlan(t, planPath) @@ -273,14 +260,11 @@ func TestLocal_planScaleOutNoDupeCount(t *testing.T) { actual := new(CountHook) b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, actual) - mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-scaleout") - defer modCleanup() - outDir := testTempDir(t) defer os.RemoveAll(outDir) - op := testOperationPlan() - op.Module = mod + op, configCleanup := testOperationPlan(t, "./test-fixtures/plan-scaleout") + defer configCleanup() op.PlanRefresh = true run, err := b.Operation(context.Background(), op) @@ -288,8 +272,8 @@ func TestLocal_planScaleOutNoDupeCount(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Err != nil { - t.Fatalf("err: %s", err) + if run.Result != backend.OperationSuccess { + t.Fatalf("plan operation failed") } expected := new(CountHook) @@ -304,10 +288,16 @@ func TestLocal_planScaleOutNoDupeCount(t *testing.T) { } } -func testOperationPlan() *backend.Operation { +func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) { + t.Helper() + + _, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir) + return &backend.Operation{ - Type: backend.OperationTypePlan, - } + Type: backend.OperationTypePlan, + ConfigDir: configDir, + ConfigLoader: configLoader, + }, configCleanup } // testPlanState is just a common state that we use for testing refresh. diff --git a/backend/local/backend_refresh.go b/backend/local/backend_refresh.go index b5ec9aa6d..3de9930bc 100644 --- a/backend/local/backend_refresh.go +++ b/backend/local/backend_refresh.go @@ -9,8 +9,8 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) func (b *Local) opRefresh( @@ -18,6 +18,9 @@ func (b *Local) opRefresh( cancelCtx context.Context, op *backend.Operation, runningOp *backend.RunningOperation) { + + var diags tfdiags.Diagnostics + // Check if our state exists if we're performing a refresh operation. We // only do this if we're managing state with this backend. if b.Backend == nil { @@ -27,26 +30,22 @@ func (b *Local) opRefresh( } if err != nil { - runningOp.Err = fmt.Errorf( - "There was an error reading the Terraform state that is needed\n"+ - "for refreshing. The path and error are shown below.\n\n"+ - "Path: %s\n\nError: %s", - b.StatePath, err) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot read state file", + fmt.Sprintf("Failed to read %s: %s", b.StatePath, err), + )) + b.ReportResult(runningOp, diags) return } } } - // If we have no config module given to use, create an empty tree to - // avoid crashes when Terraform.Context is initialized. - if op.Module == nil { - op.Module = module.NewEmptyTree() - } - // Get our context - tfCtx, opState, err := b.context(op) - if err != nil { - runningOp.Err = err + tfCtx, opState, contextDiags := b.context(op) + diags = diags.Append(contextDiags) + if contextDiags.HasErrors() { + b.ReportResult(runningOp, diags) return } @@ -76,17 +75,20 @@ func (b *Local) opRefresh( // write the resulting state to the running op runningOp.State = newState if refreshErr != nil { - runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", refreshErr) + diags = diags.Append(refreshErr) + b.ReportResult(runningOp, diags) return } // Write and persist the state if err := opState.WriteState(newState); err != nil { - runningOp.Err = errwrap.Wrapf("Error writing state: {{err}}", err) + diags = diags.Append(errwrap.Wrapf("Failed to write state: {{err}}", err)) + b.ReportResult(runningOp, diags) return } if err := opState.PersistState(); err != nil { - runningOp.Err = errwrap.Wrapf("Error saving state: {{err}}", err) + diags = diags.Append(errwrap.Wrapf("Failed to save state: {{err}}", err)) + b.ReportResult(runningOp, diags) return } } diff --git a/backend/local/backend_refresh_test.go b/backend/local/backend_refresh_test.go index b28009bd2..dc65423c2 100644 --- a/backend/local/backend_refresh_test.go +++ b/backend/local/backend_refresh_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/configs/configload" "github.com/hashicorp/terraform/terraform" ) @@ -20,11 +20,8 @@ func TestLocal_refresh(t *testing.T) { p.RefreshFn = nil p.RefreshReturn = &terraform.InstanceState{ID: "yes"} - mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh") - defer modCleanup() - - op := testOperationRefresh() - op.Module = mod + op, configCleanup := testOperationRefresh(t, "./test-fixtures/refresh") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { @@ -43,7 +40,7 @@ test_instance.foo: `) } -func TestLocal_refreshNilModule(t *testing.T) { +func TestLocal_refreshNoConfig(t *testing.T) { b, cleanup := TestLocal(t) defer cleanup() p := TestLocalProvider(t, b, "test") @@ -52,8 +49,8 @@ func TestLocal_refreshNilModule(t *testing.T) { p.RefreshFn = nil p.RefreshReturn = &terraform.InstanceState{ID: "yes"} - op := testOperationRefresh() - op.Module = nil + op, configCleanup := testOperationRefresh(t, "./test-fixtures/empty") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { @@ -84,8 +81,8 @@ func TestLocal_refreshNilModuleWithInput(t *testing.T) { b.OpInput = true - op := testOperationRefresh() - op.Module = nil + op, configCleanup := testOperationRefresh(t, "./test-fixtures/empty") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { @@ -121,15 +118,12 @@ func TestLocal_refreshInput(t *testing.T) { p.RefreshFn = nil p.RefreshReturn = &terraform.InstanceState{ID: "yes"} - mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh-var-unset") - defer modCleanup() - // Enable input asking since it is normally disabled by default b.OpInput = true b.ContextOpts.UIInput = &terraform.MockUIInput{InputReturnString: "bar"} - op := testOperationRefresh() - op.Module = mod + op, configCleanup := testOperationRefresh(t, "./test-fixtures/refresh-var-unset") + defer configCleanup() op.UIIn = b.ContextOpts.UIInput run, err := b.Operation(context.Background(), op) @@ -158,14 +152,11 @@ func TestLocal_refreshValidate(t *testing.T) { p.RefreshFn = nil p.RefreshReturn = &terraform.InstanceState{ID: "yes"} - mod, modCleanup := module.TestTree(t, "./test-fixtures/refresh") - defer modCleanup() - // Enable validation b.OpValidation = true - op := testOperationRefresh() - op.Module = mod + op, configCleanup := testOperationRefresh(t, "./test-fixtures/refresh") + defer configCleanup() run, err := b.Operation(context.Background(), op) if err != nil { @@ -184,10 +175,16 @@ test_instance.foo: `) } -func testOperationRefresh() *backend.Operation { +func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func()) { + t.Helper() + + _, configLoader, configCleanup := configload.MustLoadConfigForTests(t, configDir) + return &backend.Operation{ - Type: backend.OperationTypeRefresh, - } + Type: backend.OperationTypeRefresh, + ConfigDir: configDir, + ConfigLoader: configLoader, + }, configCleanup } // testRefreshState is just a common state that we use for testing refresh. diff --git a/backend/local/cli.go b/backend/local/cli.go index f9edfd449..f0221b7bd 100644 --- a/backend/local/cli.go +++ b/backend/local/cli.go @@ -8,6 +8,7 @@ import ( func (b *Local) CLIInit(opts *backend.CLIOpts) error { b.CLI = opts.CLI b.CLIColor = opts.CLIColor + b.ShowDiagnostics = opts.ShowDiagnostics b.ContextOpts = opts.ContextOpts b.OpInput = opts.Input b.OpValidation = opts.Validation diff --git a/backend/local/test-fixtures/empty/.gitignore b/backend/local/test-fixtures/empty/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/backend/local/testing.go b/backend/local/testing.go index 6e8711860..8c109c793 100644 --- a/backend/local/testing.go +++ b/backend/local/testing.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) // TestLocal returns a configured Local struct with temporary paths and @@ -17,15 +18,27 @@ import ( // No operations will be called on the returned value, so you can still set // public fields without any locks. func TestLocal(t *testing.T) (*Local, func()) { + t.Helper() + tempDir := testTempDir(t) - - local := New() - local.StatePath = filepath.Join(tempDir, "state.tfstate") - local.StateOutPath = filepath.Join(tempDir, "state.tfstate") - local.StateBackupPath = filepath.Join(tempDir, "state.tfstate.bak") - local.StateWorkspaceDir = filepath.Join(tempDir, "state.tfstate.d") - local.ContextOpts = &terraform.ContextOpts{} - + var local *Local + local = &Local{ + StatePath: filepath.Join(tempDir, "state.tfstate"), + StateOutPath: filepath.Join(tempDir, "state.tfstate"), + StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"), + StateWorkspaceDir: filepath.Join(tempDir, "state.tfstate.d"), + ContextOpts: &terraform.ContextOpts{}, + ShowDiagnostics: func(vals ...interface{}) { + var diags tfdiags.Diagnostics + diags = diags.Append(vals...) + for _, diag := range diags { + t.Log(diag.Description().Summary) + if local.CLI != nil { + local.CLI.Error(diag.Description().Summary) + } + } + }, + } cleanup := func() { if err := os.RemoveAll(tempDir); err != nil { t.Fatal("error clecanup up test:", err) @@ -70,7 +83,7 @@ func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResou // TestNewLocalSingle is a factory for creating a TestLocalSingleState. // This function matches the signature required for backend/init. func TestNewLocalSingle() backend.Backend { - return &TestLocalSingleState{Local: New()} + return &TestLocalSingleState{Local: &Local{}} } // TestLocalSingleState is a backend implementation that wraps Local @@ -102,7 +115,7 @@ func (b *TestLocalSingleState) DeleteState(string) error { // TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState. // This function matches the signature required for backend/init. func TestNewLocalNoDefault() backend.Backend { - return &TestLocalNoDefaultState{Local: New()} + return &TestLocalNoDefaultState{Local: &Local{}} } // TestLocalNoDefaultState is a backend implementation that wraps diff --git a/backend/nil.go b/backend/nil.go index e2f11e91d..a8dad7525 100644 --- a/backend/nil.go +++ b/backend/nil.go @@ -1,8 +1,10 @@ package backend import ( + "github.com/hashicorp/terraform/config/configschema" "github.com/hashicorp/terraform/state" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" ) // Nil is a no-op implementation of Backend. @@ -11,17 +13,15 @@ import ( // backend interface for testing. type Nil struct{} -func (Nil) Input( - ui terraform.UIInput, - c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - return c, nil +func (Nil) ConfigSchema() *configschema.Block { + return &configschema.Block{} } -func (Nil) Validate(*terraform.ResourceConfig) ([]string, []error) { - return nil, nil +func (Nil) ValidateConfig(cty.Value) tfdiags.Diagnostics { + return nil } -func (Nil) Configure(*terraform.ResourceConfig) error { +func (Nil) Configure(cty.Value) tfdiags.Diagnostics { return nil } diff --git a/backend/remote-state/artifactory/client_test.go b/backend/remote-state/artifactory/client_test.go index a7f2707d3..5d22a5f5c 100644 --- a/backend/remote-state/artifactory/client_test.go +++ b/backend/remote-state/artifactory/client_test.go @@ -4,7 +4,9 @@ import ( "testing" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/state/remote" + "github.com/zclconf/go-cty/cty" ) func TestArtifactoryClient_impl(t *testing.T) { @@ -15,18 +17,18 @@ func TestArtifactoryFactory(t *testing.T) { // This test just instantiates the client. Shouldn't make any actual // requests nor incur any costs. - config := make(map[string]interface{}) - config["url"] = "http://artifactory.local:8081/artifactory" - config["repo"] = "terraform-repo" - config["subpath"] = "myproject" + config := make(map[string]cty.Value) + config["url"] = cty.StringVal("http://artifactory.local:8081/artifactory") + config["repo"] = cty.StringVal("terraform-repo") + config["subpath"] = cty.StringVal("myproject") // For this test we'll provide the credentials as config. The // acceptance tests implicitly test passing credentials as // environment variables. - config["username"] = "test" - config["password"] = "testpass" + config["username"] = cty.StringVal("test") + config["password"] = cty.StringVal("testpass") - b := backend.TestBackendConfig(t, New(), config) + b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", config)) state, err := b.State(backend.DefaultStateName) if err != nil { diff --git a/backend/remote-state/azure/backend_test.go b/backend/remote-state/azure/backend_test.go index ae139db7b..9c8f69a20 100644 --- a/backend/remote-state/azure/backend_test.go +++ b/backend/remote-state/azure/backend_test.go @@ -40,7 +40,7 @@ func TestBackendConfig(t *testing.T) { "access_key": "QUNDRVNTX0tFWQ0K", } - b := backend.TestBackendConfig(t, New(), config).(*Backend) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) if b.containerName != "tfcontainer" { t.Fatalf("Incorrect bucketName was populated") @@ -57,12 +57,12 @@ func TestBackend(t *testing.T) { res := setupResources(t, keyName) defer destroyResources(t, res.resourceGroupName) - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "storage_account_name": res.storageAccountName, "container_name": res.containerName, "key": keyName, "access_key": res.accessKey, - }).(*Backend) + })).(*Backend) backend.TestBackendStates(t, b) } @@ -74,19 +74,19 @@ func TestBackendLocked(t *testing.T) { res := setupResources(t, keyName) defer destroyResources(t, res.resourceGroupName) - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "storage_account_name": res.storageAccountName, "container_name": res.containerName, "key": keyName, "access_key": res.accessKey, - }).(*Backend) + })).(*Backend) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "storage_account_name": res.storageAccountName, "container_name": res.containerName, "key": keyName, "access_key": res.accessKey, - }).(*Backend) + })).(*Backend) backend.TestBackendStateLocks(t, b1, b2) backend.TestBackendStateForceUnlock(t, b1, b2) diff --git a/backend/remote-state/azure/client_test.go b/backend/remote-state/azure/client_test.go index 5ca0a4ac9..1e7f09b2b 100644 --- a/backend/remote-state/azure/client_test.go +++ b/backend/remote-state/azure/client_test.go @@ -21,12 +21,12 @@ func TestRemoteClient(t *testing.T) { res := setupResources(t, keyName) defer destroyResources(t, res.resourceGroupName) - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "storage_account_name": res.storageAccountName, "container_name": res.containerName, "key": keyName, "access_key": res.accessKey, - }).(*Backend) + })).(*Backend) state, err := b.State(backend.DefaultStateName) if err != nil { @@ -43,19 +43,19 @@ func TestRemoteClientLocks(t *testing.T) { res := setupResources(t, keyName) defer destroyResources(t, res.resourceGroupName) - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "storage_account_name": res.storageAccountName, "container_name": res.containerName, "key": keyName, "access_key": res.accessKey, - }).(*Backend) + })).(*Backend) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "storage_account_name": res.storageAccountName, "container_name": res.containerName, "key": keyName, "access_key": res.accessKey, - }).(*Backend) + })).(*Backend) s1, err := b1.State(backend.DefaultStateName) if err != nil { diff --git a/backend/remote-state/backend.go b/backend/remote-state/backend.go index b0f546a77..5f9c9a0d7 100644 --- a/backend/remote-state/backend.go +++ b/backend/remote-state/backend.go @@ -10,7 +10,8 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" ) // Backend implements backend.Backend for remote state backends. @@ -30,7 +31,8 @@ type Backend struct { client remote.Client } -func (b *Backend) Configure(rc *terraform.ResourceConfig) error { +func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { + // Set our configureFunc manually b.Backend.ConfigureFunc = func(ctx context.Context) error { c, err := b.ConfigureFunc(ctx) @@ -43,8 +45,7 @@ func (b *Backend) Configure(rc *terraform.ResourceConfig) error { return nil } - // Run the normal configuration - return b.Backend.Configure(rc) + return b.Backend.Configure(obj) } func (b *Backend) States() ([]string, error) { diff --git a/backend/remote-state/consul/backend_test.go b/backend/remote-state/consul/backend_test.go index b5870ad5d..6aa43349a 100644 --- a/backend/remote-state/consul/backend_test.go +++ b/backend/remote-state/consul/backend_test.go @@ -52,15 +52,15 @@ func TestBackend(t *testing.T) { path := fmt.Sprintf("tf-unit/%s", time.Now().String()) // Get the backend. We need two to test locking. - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path, - }) + })) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path, - }) + })) // Test backend.TestBackendStates(t, b1) @@ -71,17 +71,17 @@ func TestBackend_lockDisabled(t *testing.T) { path := fmt.Sprintf("tf-unit/%s", time.Now().String()) // Get the backend. We need two to test locking. - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path, "lock": false, - }) + })) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path + "different", // Diff so locking test would fail if it was locking "lock": false, - }) + })) // Test backend.TestBackendStates(t, b1) @@ -90,11 +90,11 @@ func TestBackend_lockDisabled(t *testing.T) { func TestBackend_gzip(t *testing.T) { // Get the backend - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": fmt.Sprintf("tf-unit/%s", time.Now().String()), "gzip": true, - }) + })) // Test backend.TestBackendStates(t, b) diff --git a/backend/remote-state/consul/client_test.go b/backend/remote-state/consul/client_test.go index b2c95c527..ab36635e4 100644 --- a/backend/remote-state/consul/client_test.go +++ b/backend/remote-state/consul/client_test.go @@ -20,10 +20,10 @@ func TestRemoteClient_impl(t *testing.T) { func TestRemoteClient(t *testing.T) { // Get the backend - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": fmt.Sprintf("tf-unit/%s", time.Now().String()), - }) + })) // Grab the client state, err := b.State(backend.DefaultStateName) @@ -40,10 +40,10 @@ func TestRemoteClient_gzipUpgrade(t *testing.T) { statePath := fmt.Sprintf("tf-unit/%s", time.Now().String()) // Get the backend - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": statePath, - }) + })) // Grab the client state, err := b.State(backend.DefaultStateName) @@ -55,11 +55,11 @@ func TestRemoteClient_gzipUpgrade(t *testing.T) { remote.TestClient(t, state.(*remote.State).Client) // create a new backend with gzip - b = backend.TestBackendConfig(t, New(), map[string]interface{}{ + b = backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": statePath, "gzip": true, - }) + })) // Grab the client state, err = b.State(backend.DefaultStateName) @@ -75,18 +75,18 @@ func TestConsul_stateLock(t *testing.T) { path := fmt.Sprintf("tf-unit/%s", time.Now().String()) // create 2 instances to get 2 remote.Clients - sA, err := backend.TestBackendConfig(t, New(), map[string]interface{}{ + sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path, - }).State(backend.DefaultStateName) + })).State(backend.DefaultStateName) if err != nil { t.Fatal(err) } - sB, err := backend.TestBackendConfig(t, New(), map[string]interface{}{ + sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path, - }).State(backend.DefaultStateName) + })).State(backend.DefaultStateName) if err != nil { t.Fatal(err) } @@ -96,10 +96,10 @@ func TestConsul_stateLock(t *testing.T) { func TestConsul_destroyLock(t *testing.T) { // Get the backend - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": fmt.Sprintf("tf-unit/%s", time.Now().String()), - }) + })) // Grab the client s, err := b.State(backend.DefaultStateName) @@ -135,18 +135,18 @@ func TestConsul_lostLock(t *testing.T) { path := fmt.Sprintf("tf-unit/%s", time.Now().String()) // create 2 instances to get 2 remote.Clients - sA, err := backend.TestBackendConfig(t, New(), map[string]interface{}{ + sA, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path, - }).State(backend.DefaultStateName) + })).State(backend.DefaultStateName) if err != nil { t.Fatal(err) } - sB, err := backend.TestBackendConfig(t, New(), map[string]interface{}{ + sB, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path + "-not-used", - }).State(backend.DefaultStateName) + })).State(backend.DefaultStateName) if err != nil { t.Fatal(err) } @@ -190,10 +190,10 @@ func TestConsul_lostLockConnection(t *testing.T) { path := fmt.Sprintf("tf-unit/%s", time.Now().String()) - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "address": srv.HTTPAddr, "path": path, - }) + })) s, err := b.State(backend.DefaultStateName) if err != nil { diff --git a/backend/remote-state/etcdv2/client_test.go b/backend/remote-state/etcdv2/client_test.go index e37b5753b..ae6e6b3d9 100644 --- a/backend/remote-state/etcdv2/client_test.go +++ b/backend/remote-state/etcdv2/client_test.go @@ -7,7 +7,9 @@ import ( "time" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/state/remote" + "github.com/zclconf/go-cty/cty" ) func TestEtcdClient_impl(t *testing.T) { @@ -21,19 +23,19 @@ func TestEtcdClient(t *testing.T) { } // Get the backend - config := map[string]interface{}{ - "endpoints": endpoint, - "path": fmt.Sprintf("tf-unit/%s", time.Now().String()), + config := map[string]cty.Value{ + "endpoints": cty.StringVal(endpoint), + "path": cty.StringVal(fmt.Sprintf("tf-unit/%s", time.Now().String())), } if username := os.Getenv("ETCD_USERNAME"); username != "" { - config["username"] = username + config["username"] = cty.StringVal(username) } if password := os.Getenv("ETCD_PASSWORD"); password != "" { - config["password"] = password + config["password"] = cty.StringVal(password) } - b := backend.TestBackendConfig(t, New(), config) + b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", config)) state, err := b.State(backend.DefaultStateName) if err != nil { t.Fatalf("Error for valid config: %s", err) diff --git a/backend/remote-state/etcdv3/backend_test.go b/backend/remote-state/etcdv3/backend_test.go index ae3e3e645..1199dc978 100644 --- a/backend/remote-state/etcdv3/backend_test.go +++ b/backend/remote-state/etcdv3/backend_test.go @@ -55,15 +55,15 @@ func TestBackend(t *testing.T) { prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339)) // Get the backend. We need two to test locking. - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix, - }) + })) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix, - }) + })) // Test backend.TestBackendStates(t, b1) @@ -78,17 +78,17 @@ func TestBackend_lockDisabled(t *testing.T) { prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339)) // Get the backend. We need two to test locking. - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix, "lock": false, - }) + })) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix + "/" + "different", // Diff so locking test would fail if it was locking "lock": false, - }) + })) // Test backend.TestBackendStateLocks(t, b1, b2) diff --git a/backend/remote-state/etcdv3/client_test.go b/backend/remote-state/etcdv3/client_test.go index f30a25b5b..057d80900 100644 --- a/backend/remote-state/etcdv3/client_test.go +++ b/backend/remote-state/etcdv3/client_test.go @@ -22,10 +22,10 @@ func TestRemoteClient(t *testing.T) { prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339)) // Get the backend - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix, - }) + })) // Grab the client state, err := b.State(backend.DefaultStateName) @@ -44,18 +44,18 @@ func TestEtcdv3_stateLock(t *testing.T) { prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339)) // Get the backend - s1, err := backend.TestBackendConfig(t, New(), map[string]interface{}{ + s1, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix, - }).State(backend.DefaultStateName) + })).State(backend.DefaultStateName) if err != nil { t.Fatal(err) } - s2, err := backend.TestBackendConfig(t, New(), map[string]interface{}{ + s2, err := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix, - }).State(backend.DefaultStateName) + })).State(backend.DefaultStateName) if err != nil { t.Fatal(err) } @@ -70,10 +70,10 @@ func TestEtcdv3_destroyLock(t *testing.T) { prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339)) // Get the backend - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "endpoints": etcdv3Endpoints, "prefix": prefix, - }) + })) // Grab the client s, err := b.State(backend.DefaultStateName) diff --git a/backend/remote-state/gcs/backend_test.go b/backend/remote-state/gcs/backend_test.go index 61d27da8c..9203bb877 100644 --- a/backend/remote-state/gcs/backend_test.go +++ b/backend/remote-state/gcs/backend_test.go @@ -187,7 +187,7 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend { "encryption_key": key, } - b := backend.TestBackendConfig(t, New(), config) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)) be := b.(*Backend) // create the bucket if it doesn't exist diff --git a/backend/remote-state/http/backend_test.go b/backend/remote-state/http/backend_test.go index 4c99204f9..dad47c17c 100644 --- a/backend/remote-state/http/backend_test.go +++ b/backend/remote-state/http/backend_test.go @@ -3,6 +3,9 @@ package http import ( "testing" + "github.com/hashicorp/terraform/configs" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" ) @@ -13,16 +16,16 @@ func TestBackend_impl(t *testing.T) { func TestHTTPClientFactory(t *testing.T) { // defaults - conf := map[string]interface{}{ - "address": "http://127.0.0.1:8888/foo", + conf := map[string]cty.Value{ + "address": cty.StringVal("http://127.0.0.1:8888/foo"), } - b := backend.TestBackendConfig(t, New(), conf).(*Backend) + b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend) client := b.client if client == nil { t.Fatal("Unexpected failure, address") } - if client.URL.String() != conf["address"] { + if client.URL.String() != "http://127.0.0.1:8888/foo" { t.Fatalf("Expected address \"%s\", got \"%s\"", conf["address"], client.URL.String()) } if client.UpdateMethod != "POST" { @@ -39,18 +42,18 @@ func TestHTTPClientFactory(t *testing.T) { } // custom - conf = map[string]interface{}{ - "address": "http://127.0.0.1:8888/foo", - "update_method": "BLAH", - "lock_address": "http://127.0.0.1:8888/bar", - "lock_method": "BLIP", - "unlock_address": "http://127.0.0.1:8888/baz", - "unlock_method": "BLOOP", - "username": "user", - "password": "pass", + conf = map[string]cty.Value{ + "address": cty.StringVal("http://127.0.0.1:8888/foo"), + "update_method": cty.StringVal("BLAH"), + "lock_address": cty.StringVal("http://127.0.0.1:8888/bar"), + "lock_method": cty.StringVal("BLIP"), + "unlock_address": cty.StringVal("http://127.0.0.1:8888/baz"), + "unlock_method": cty.StringVal("BLOOP"), + "username": cty.StringVal("user"), + "password": cty.StringVal("pass"), } - b = backend.TestBackendConfig(t, New(), conf).(*Backend) + b = backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend) client = b.client if client == nil { @@ -59,13 +62,13 @@ func TestHTTPClientFactory(t *testing.T) { if client.UpdateMethod != "BLAH" { t.Fatalf("Expected update_method \"%s\", got \"%s\"", "BLAH", client.UpdateMethod) } - if client.LockURL.String() != conf["lock_address"] || client.LockMethod != "BLIP" { + if client.LockURL.String() != conf["lock_address"].AsString() || client.LockMethod != "BLIP" { t.Fatalf("Unexpected lock_address \"%s\" vs \"%s\" or lock_method \"%s\" vs \"%s\"", client.LockURL.String(), - conf["lock_address"], client.LockMethod, conf["lock_method"]) + conf["lock_address"].AsString(), client.LockMethod, conf["lock_method"]) } - if client.UnlockURL.String() != conf["unlock_address"] || client.UnlockMethod != "BLOOP" { + if client.UnlockURL.String() != conf["unlock_address"].AsString() || client.UnlockMethod != "BLOOP" { t.Fatalf("Unexpected unlock_address \"%s\" vs \"%s\" or unlock_method \"%s\" vs \"%s\"", client.UnlockURL.String(), - conf["unlock_address"], client.UnlockMethod, conf["unlock_method"]) + conf["unlock_address"].AsString(), client.UnlockMethod, conf["unlock_method"]) } if client.Username != "user" || client.Password != "pass" { t.Fatalf("Unexpected username \"%s\" vs \"%s\" or password \"%s\" vs \"%s\"", client.Username, conf["username"], diff --git a/backend/remote-state/inmem/backend_test.go b/backend/remote-state/inmem/backend_test.go index fbbe1e277..d2e08621a 100644 --- a/backend/remote-state/inmem/backend_test.go +++ b/backend/remote-state/inmem/backend_test.go @@ -3,6 +3,7 @@ package inmem import ( "testing" + "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/terraform" @@ -20,7 +21,7 @@ func TestBackendConfig(t *testing.T) { "lock_id": testID, } - b := backend.TestBackendConfig(t, New(), config).(*Backend) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) s, err := b.State(backend.DefaultStateName) if err != nil { @@ -39,14 +40,14 @@ func TestBackendConfig(t *testing.T) { func TestBackend(t *testing.T) { defer Reset() - b := backend.TestBackendConfig(t, New(), nil).(*Backend) + b := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).(*Backend) backend.TestBackendStates(t, b) } func TestBackendLocked(t *testing.T) { defer Reset() - b1 := backend.TestBackendConfig(t, New(), nil).(*Backend) - b2 := backend.TestBackendConfig(t, New(), nil).(*Backend) + b1 := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).(*Backend) + b2 := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).(*Backend) backend.TestBackendStateLocks(t, b1, b2) } @@ -54,7 +55,7 @@ func TestBackendLocked(t *testing.T) { // use the this backen to test the remote.State implementation func TestRemoteState(t *testing.T) { defer Reset() - b := backend.TestBackendConfig(t, New(), nil) + b := backend.TestBackendConfig(t, New(), hcl.EmptyBody()) workspace := "workspace" diff --git a/backend/remote-state/inmem/client_test.go b/backend/remote-state/inmem/client_test.go index 3a0fa8f30..1eabd1de3 100644 --- a/backend/remote-state/inmem/client_test.go +++ b/backend/remote-state/inmem/client_test.go @@ -3,6 +3,7 @@ package inmem import ( "testing" + "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/state/remote" ) @@ -14,7 +15,7 @@ func TestRemoteClient_impl(t *testing.T) { func TestRemoteClient(t *testing.T) { defer Reset() - b := backend.TestBackendConfig(t, New(), nil) + b := backend.TestBackendConfig(t, New(), hcl.EmptyBody()) s, err := b.State(backend.DefaultStateName) if err != nil { @@ -26,7 +27,7 @@ func TestRemoteClient(t *testing.T) { func TestInmemLocks(t *testing.T) { defer Reset() - s, err := backend.TestBackendConfig(t, New(), nil).State(backend.DefaultStateName) + s, err := backend.TestBackendConfig(t, New(), hcl.EmptyBody()).State(backend.DefaultStateName) if err != nil { t.Fatal(err) } diff --git a/backend/remote-state/manta/backend_test.go b/backend/remote-state/manta/backend_test.go index 76f2b7377..f10a14239 100644 --- a/backend/remote-state/manta/backend_test.go +++ b/backend/remote-state/manta/backend_test.go @@ -30,10 +30,10 @@ func TestBackend(t *testing.T) { directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) keyName := "testState" - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ - "path": directory, - "object_name": keyName, - }).(*Backend) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "path": directory, + "objectName": keyName, + })).(*Backend) createMantaFolder(t, b.storageClient, directory) defer deleteMantaFolder(t, b.storageClient, directory) @@ -47,15 +47,15 @@ func TestBackendLocked(t *testing.T) { directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) keyName := "testState" - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ - "path": directory, - "object_name": keyName, - }).(*Backend) + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "path": directory, + "objectName": keyName, + })).(*Backend) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ - "path": directory, - "object_name": keyName, - }).(*Backend) + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "path": directory, + "objectName": keyName, + })).(*Backend) createMantaFolder(t, b1.storageClient, directory) defer deleteMantaFolder(t, b1.storageClient, directory) diff --git a/backend/remote-state/manta/client_test.go b/backend/remote-state/manta/client_test.go index 18847972f..cc465239a 100644 --- a/backend/remote-state/manta/client_test.go +++ b/backend/remote-state/manta/client_test.go @@ -20,10 +20,10 @@ func TestRemoteClient(t *testing.T) { directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) keyName := "testState" - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ - "path": directory, - "object_name": keyName, - }).(*Backend) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "path": directory, + "objectName": keyName, + })).(*Backend) createMantaFolder(t, b.storageClient, directory) defer deleteMantaFolder(t, b.storageClient, directory) @@ -41,15 +41,15 @@ func TestRemoteClientLocks(t *testing.T) { directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) keyName := "testState" - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ - "path": directory, - "object_name": keyName, - }).(*Backend) + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "path": directory, + "objectName": keyName, + })).(*Backend) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ - "path": directory, - "object_name": keyName, - }).(*Backend) + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "path": directory, + "objectName": keyName, + })).(*Backend) createMantaFolder(t, b1.storageClient, directory) defer deleteMantaFolder(t, b1.storageClient, directory) diff --git a/backend/remote-state/s3/backend_test.go b/backend/remote-state/s3/backend_test.go index ae6ca3475..72c137b77 100644 --- a/backend/remote-state/s3/backend_test.go +++ b/backend/remote-state/s3/backend_test.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/terraform" ) @@ -42,7 +42,7 @@ func TestBackendConfig(t *testing.T) { "dynamodb_table": "dynamoTable", } - b := backend.TestBackendConfig(t, New(), config).(*Backend) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) if *b.s3Client.Config.Region != "us-west-1" { t.Fatalf("Incorrect region was populated") @@ -68,22 +68,16 @@ func TestBackendConfig(t *testing.T) { func TestBackendConfig_invalidKey(t *testing.T) { testACC(t) - cfg := map[string]interface{}{ + cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ "region": "us-west-1", "bucket": "tf-test", "key": "/leading-slash", "encrypt": true, "dynamodb_table": "dynamoTable", - } + }) - rawCfg, err := config.NewRawConfig(cfg) - if err != nil { - t.Fatal(err) - } - resCfg := terraform.NewResourceConfig(rawCfg) - - _, errs := New().Validate(resCfg) - if len(errs) != 1 { + diags := New().ValidateConfig(cfg) + if !diags.HasErrors() { t.Fatal("expected config validation error") } } @@ -94,11 +88,11 @@ func TestBackend(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b.s3Client, bucketName) defer deleteS3Bucket(t, b.s3Client, bucketName) @@ -112,19 +106,19 @@ func TestBackendLocked(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "test/state" - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b1.s3Client, bucketName) defer deleteS3Bucket(t, b1.s3Client, bucketName) @@ -141,11 +135,11 @@ func TestBackendExtraPaths(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "test/state/tfstate" - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b.s3Client, bucketName) defer deleteS3Bucket(t, b.s3Client, bucketName) @@ -256,11 +250,11 @@ func TestBackendPrefixInWorkspace(t *testing.T) { testACC(t) bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ - "bucket": bucketName, - "key": "test-env.tfstate", + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": "test-env.tfstate", "workspace_key_prefix": "env", - }).(*Backend) + })).(*Backend) createS3Bucket(t, b.s3Client, bucketName) defer deleteS3Bucket(t, b.s3Client, bucketName) @@ -284,33 +278,33 @@ func TestKeyEnv(t *testing.T) { keyName := "some/paths/tfstate" bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix()) - b0 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucket0Name, "key": keyName, "encrypt": true, "workspace_key_prefix": "", - }).(*Backend) + })).(*Backend) createS3Bucket(t, b0.s3Client, bucket0Name) defer deleteS3Bucket(t, b0.s3Client, bucket0Name) bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix()) - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucket1Name, "key": keyName, "encrypt": true, "workspace_key_prefix": "project/env:", - }).(*Backend) + })).(*Backend) createS3Bucket(t, b1.s3Client, bucket1Name) defer deleteS3Bucket(t, b1.s3Client, bucket1Name) bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix()) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucket2Name, "key": keyName, "encrypt": true, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b2.s3Client, bucket2Name) defer deleteS3Bucket(t, b2.s3Client, bucket2Name) diff --git a/backend/remote-state/s3/client_test.go b/backend/remote-state/s3/client_test.go index e981924b7..9b7831681 100644 --- a/backend/remote-state/s3/client_test.go +++ b/backend/remote-state/s3/client_test.go @@ -24,11 +24,11 @@ func TestRemoteClient(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b.s3Client, bucketName) defer deleteS3Bucket(t, b.s3Client, bucketName) @@ -46,19 +46,19 @@ func TestRemoteClientLocks(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b1.s3Client, bucketName) defer deleteS3Bucket(t, b1.s3Client, bucketName) @@ -84,19 +84,19 @@ func TestForceUnlock(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-force-%x", time.Now().Unix()) keyName := "testState" - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "encrypt": true, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b1.s3Client, bucketName) defer deleteS3Bucket(t, b1.s3Client, bucketName) @@ -161,11 +161,11 @@ func TestRemoteClient_clientMD5(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b.s3Client, bucketName) defer deleteS3Bucket(t, b.s3Client, bucketName) @@ -209,11 +209,11 @@ func TestRemoteClient_stateChecksum(t *testing.T) { bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" - b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, "dynamodb_table": bucketName, - }).(*Backend) + })).(*Backend) createS3Bucket(t, b1.s3Client, bucketName) defer deleteS3Bucket(t, b1.s3Client, bucketName) @@ -240,10 +240,10 @@ func TestRemoteClient_stateChecksum(t *testing.T) { // Use b2 without a dynamodb_table to bypass the lock table to write the state directly. // client2 will write the "incorrect" state, simulating s3 eventually consistency delays - b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "key": keyName, - }).(*Backend) + })).(*Backend) s2, err := b2.State(backend.DefaultStateName) if err != nil { t.Fatal(err) diff --git a/backend/remote-state/swift/backend_test.go b/backend/remote-state/swift/backend_test.go index 85d505fc6..39c7c8997 100644 --- a/backend/remote-state/swift/backend_test.go +++ b/backend/remote-state/swift/backend_test.go @@ -46,7 +46,7 @@ func TestBackendConfig(t *testing.T) { "container": "test-tfstate", } - b := backend.TestBackendConfig(t, New(), config).(*Backend) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) if b.container != "test-tfstate" { t.Fatal("Incorrect path was provided.") @@ -61,9 +61,9 @@ func TestBackend(t *testing.T) { container := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix()) - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "container": container, - }).(*Backend) + })).(*Backend) defer deleteSwiftContainer(t, b.client, container) @@ -75,9 +75,9 @@ func TestBackendPath(t *testing.T) { path := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix()) t.Logf("[DEBUG] Generating backend config") - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "path": path, - }).(*Backend) + })).(*Backend) t.Logf("[DEBUG] Backend configured") defer deleteSwiftContainer(t, b.client, path) @@ -131,10 +131,10 @@ func TestBackendArchive(t *testing.T) { container := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix()) archiveContainer := fmt.Sprintf("%s_archive", container) - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "archive_container": archiveContainer, "container": container, - }).(*Backend) + })).(*Backend) defer deleteSwiftContainer(t, b.client, container) defer deleteSwiftContainer(t, b.client, archiveContainer) diff --git a/backend/remote-state/swift/client_test.go b/backend/remote-state/swift/client_test.go index a550af08a..eee3ae3c0 100644 --- a/backend/remote-state/swift/client_test.go +++ b/backend/remote-state/swift/client_test.go @@ -18,9 +18,9 @@ func TestRemoteClient(t *testing.T) { container := fmt.Sprintf("terraform-state-swift-test-%x", time.Now().Unix()) - b := backend.TestBackendConfig(t, New(), map[string]interface{}{ + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ "container": container, - }).(*Backend) + })).(*Backend) state, err := b.State(backend.DefaultStateName) if err != nil { diff --git a/backend/testing.go b/backend/testing.go index 22dc99791..d0f32b995 100644 --- a/backend/testing.go +++ b/backend/testing.go @@ -5,41 +5,61 @@ import ( "sort" "testing" + "github.com/hashicorp/terraform/configs" + + "github.com/hashicorp/terraform/config/hcl2shim" + + "github.com/hashicorp/terraform/tfdiags" + + "github.com/hashicorp/hcl2/hcldec" + uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/terraform/config" + "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" ) // TestBackendConfig validates and configures the backend with the // given configuration. -func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backend { +func TestBackendConfig(t *testing.T, b Backend, c hcl.Body) Backend { t.Helper() - // Get the proper config structure - rc, err := config.NewRawConfig(c) - if err != nil { - t.Fatalf("bad: %s", err) - } - conf := terraform.NewResourceConfig(rc) + t.Logf("TestBackendConfig on %T with %#v", b, c) - // Validate - warns, errs := b.Validate(conf) - if len(warns) > 0 { - t.Fatalf("warnings: %s", warns) - } - if len(errs) > 0 { - t.Fatalf("errors: %s", errs) + var diags tfdiags.Diagnostics + + schema := b.ConfigSchema() + spec := schema.DecoderSpec() + obj, decDiags := hcldec.Decode(c, spec, nil) + diags = diags.Append(decDiags) + + valDiags := b.ValidateConfig(obj) + diags = diags.Append(valDiags.InConfigBody(c)) + + if len(diags) != 0 { + t.Fatal(diags) } - // Configure - if err := b.Configure(conf); err != nil { - t.Fatalf("err: %s", err) + confDiags := b.Configure(obj) + if len(confDiags) != 0 { + confDiags = confDiags.InConfigBody(c) + t.Fatal(confDiags) } return b } +// TestWrapConfig takes a raw data structure and converts it into a +// synthetic hcl.Body to use for testing. +// +// The given structure should only include values that can be accepted by +// hcl2shim.HCL2ValueFromConfigValue. If incompatible values are given, +// this function will panic. +func TestWrapConfig(raw map[string]interface{}) hcl.Body { + obj := hcl2shim.HCL2ValueFromConfigValue(raw) + return configs.SynthBody("", obj.AsValueMap()) +} + // TestBackend will test the functionality of a Backend. The backend is // assumed to already be configured. This will test state functionality. // If the backend reports it doesn't support multi-state by returning the diff --git a/builtin/providers/terraform/data_source_state.go b/builtin/providers/terraform/data_source_state.go index a822eea8d..536f8b2d4 100644 --- a/builtin/providers/terraform/data_source_state.go +++ b/builtin/providers/terraform/data_source_state.go @@ -1,497 +1,116 @@ package terraform import ( - "fmt" - "log" - "time" + "fmt" + "log" + "time" - multierror "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/backend" - backendInit "github.com/hashicorp/terraform/backend/init" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/helper/schema" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/backend" + backendinit "github.com/hashicorp/terraform/backend/init" + "github.com/hashicorp/terraform/config/hcl2shim" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/tfdiags" ) func dataSourceRemoteState() *schema.Resource { - return &schema.Resource{ - Read: dataSourceRemoteStateRead, + return &schema.Resource{ + Read: dataSourceRemoteStateRead, - Schema: map[string]*schema.Schema{ - "backend": { - Type: schema.TypeString, - Required: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - if vStr, ok := v.(string); ok && vStr == "_local" { - ws = append(ws, "Use of the %q backend is now officially "+ - "supported as %q. Please update your configuration to ensure "+ - "compatibility with future versions of Terraform.", - "_local", "local") - } + Schema: map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Required: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + if vStr, ok := v.(string); ok && vStr == "_local" { + ws = append(ws, "Use of the %q backend is now officially "+ + "supported as %q. Please update your configuration to ensure "+ + "compatibility with future versions of Terraform.", + "_local", "local") + } - return - }, - }, + return + }, + }, - // This field now contains all possible attributes that are supported - // by any of the existing backends. When merging this into 0.12 this - // should be reverted and instead the new 'cty.DynamicPseudoType' type - // should be used to make this work with any future backends as well. - "config": { - Type: schema.TypeSet, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "path": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "hostname": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "organization": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "token": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "workspaces": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Schema{Type: schema.TypeMap}, - }, - "username": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "password": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "url": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "repo": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "subpath": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "storage_account_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "container_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "key": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "access_key": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "environment": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "resource_group_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "arm_subscription_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "arm_client_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "arm_client_secret": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "arm_tenant_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "access_token": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "address": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "scheme": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "datacenter": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "http_auth": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "gzip": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "lock": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "ca_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "cert_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "key_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "endpoints": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "prefix": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "cacert_path": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "cert_path": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "key_path": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "bucket": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "credentials": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "project": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "region": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "encryption_key": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "update_method": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "lock_address": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "lock_method": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "unlock_address": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "unlock_method": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "skip_cert_verification": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "account": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "user": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "key_material": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "key_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "insecure_skip_tls_verify": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "object_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "endpoint": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "encrypt": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "acl": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "secret_key": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "kms_key_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "lock_table": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "dynamodb_table": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "profile": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "shared_credentials_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "role_arn": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "assume_role_policy": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "external_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "session_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "workspace_key_prefix": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "skip_credentials_validation": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "skip_get_ec2_platforms": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "skip_region_validation": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "skip_requesting_account_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "skip_metadata_api_check": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "auth_url": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "container": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "user_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "user_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "region_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "tenant_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "tenant_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "domain_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "domain_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "insecure": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "cacert_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "cert": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "archive_container": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "archive_path": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "expire_after": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - }, - }, - }, + "config": { + Type: schema.TypeMap, + Optional: true, + }, - "defaults": { - Type: schema.TypeMap, - Optional: true, - }, + "defaults": { + Type: schema.TypeMap, + Optional: true, + }, - "environment": { - Type: schema.TypeString, - Optional: true, - Default: backend.DefaultStateName, - Deprecated: "Terraform environments are now called workspaces. Please use the workspace key instead.", - }, + "environment": { + Type: schema.TypeString, + Optional: true, + Default: backend.DefaultStateName, + Deprecated: "Terraform environments are now called workspaces. Please use the workspace key instead.", + }, - "workspace": { - Type: schema.TypeString, - Optional: true, - Default: backend.DefaultStateName, - }, + "workspace": { + Type: schema.TypeString, + Optional: true, + Default: backend.DefaultStateName, + }, - "__has_dynamic_attributes": { - Type: schema.TypeString, - Optional: true, - }, - }, - } + "__has_dynamic_attributes": { + Type: schema.TypeString, + Optional: true, + }, + }, + } } func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error { - backendType := d.Get("backend").(string) + backendType := d.Get("backend").(string) - // Get the configuration in a type we want. This is a bit of a hack but makes - // things work for the 'remote' backend as well. This can simply be deleted or - // reverted when merging this 0.12. - raw := make(map[string]interface{}) - if cfg, ok := d.GetOk("config"); ok { - if raw, ok = cfg.(*schema.Set).List()[0].(map[string]interface{}); ok { - for k, v := range raw { - switch v := v.(type) { - case string: - if v == "" { - delete(raw, k) - } - case []interface{}: - if len(v) == 0 { - delete(raw, k) - } - } - } - } - } - - rawConfig, err := config.NewRawConfig(raw) - if err != nil { - return fmt.Errorf("error initializing backend: %s", err) - } - - // Don't break people using the old _local syntax - but note warning above - if backendType == "_local" { - log.Println(`[INFO] Switching old (unsupported) backend "_local" to "local"`) - backendType = "local" + // Don't break people using the old _local syntax - but note warning above + if backendType == "_local" { + log.Println(`[INFO] Switching old (unsupported) backend "_local" to "local"`) + backendType = "local" } // Create the client to access our remote state log.Printf("[DEBUG] Initializing remote state backend: %s", backendType) - f := backendInit.Backend(backendType) + f := backendinit.Backend(backendType) if f == nil { - return fmt.Errorf("Unknown backend type: %s", backendType) + return fmt.Errorf("Unknown backend type: %s", backendType) } b := f() - warns, errs := b.Validate(terraform.NewResourceConfig(rawConfig)) - for _, warning := range warns { - log.Printf("[DEBUG] Warning validating backend config: %s", warning) - } - if len(errs) > 0 { - return fmt.Errorf("error validating backend config: %s", multierror.Append(nil, errs...)) - } + schema := b.ConfigSchema() + rawConfig := d.Get("config") + configVal := hcl2shim.HCL2ValueFromConfigValue(rawConfig) - // Configure the backend - if err := b.Configure(terraform.NewResourceConfig(rawConfig)); err != nil { - return fmt.Errorf("error initializing backend: %s", err) + // Try to coerce the provided value into the desired configuration type. + configVal, err := schema.CoerceValue(configVal) + if err != nil { + return fmt.Errorf("invalid %s backend configuration: %s", backendType, tfdiags.FormatError(err)) + } + validateDiags := b.ValidateConfig(configVal) + if validateDiags.HasErrors() { + return validateDiags.Err() + } + configureDiags := b.Configure(configVal) + if configureDiags.HasErrors() { + return configureDiags.Err() } // environment is deprecated in favour of workspace. // If both keys are set workspace should win. name := d.Get("environment").(string) if ws, ok := d.GetOk("workspace"); ok && ws != backend.DefaultStateName { - name = ws.(string) + name = ws.(string) } state, err := b.State(name) if err != nil { - return fmt.Errorf("error loading the remote state: %s", err) + return fmt.Errorf("error loading the remote state: %s", err) } if err := state.RefreshState(); err != nil { - return err + return err } d.SetId(time.Now().UTC().String()) @@ -499,24 +118,24 @@ func dataSourceRemoteStateRead(d *schema.ResourceData, meta interface{}) error { defaults := d.Get("defaults").(map[string]interface{}) for key, val := range defaults { - outputMap[key] = val + outputMap[key] = val } remoteState := state.State() if remoteState.Empty() { - log.Println("[DEBUG] empty remote state") + log.Println("[DEBUG] empty remote state") } else { - for key, val := range remoteState.RootModule().Outputs { - if val.Value != nil { - outputMap[key] = val.Value + for key, val := range remoteState.RootModule().Outputs { + if val.Value != nil { + outputMap[key] = val.Value + } } - } } mappedOutputs := remoteStateFlatten(outputMap) for key, val := range mappedOutputs { - d.UnsafeSetFieldRaw(key, val) + d.UnsafeSetFieldRaw(key, val) } return nil diff --git a/command/meta_backend.go b/command/meta_backend.go index 067f5cc8c..e87c6d551 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/mapstructure" @@ -27,9 +28,9 @@ import ( // BackendOpts are the options used to initialize a backend.Backend. type BackendOpts struct { - // Module is the root module from which we will extract the terraform and - // backend configuration. - Config *config.Config + // Config is a representation of the backend configuration block given in + // the root module, or nil if no such block is present. + Config *configs.Backend // ConfigFile is a path to a file that contains configuration that // is merged directly into the backend configuration when loaded diff --git a/command/meta_config.go b/command/meta_config.go index d14740ced..66e4fe683 100644 --- a/command/meta_config.go +++ b/command/meta_config.go @@ -80,6 +80,25 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic return module, diags } +// loadBackendConfig reads configuration from the given directory and returns +// the backend configuration defined by that module, if any. Nil is returned +// if the specified module does not have an explicit backend configuration. +// +// This is a convenience method for command code that will delegate to the +// configured backend to do most of its work, since in that case it is the +// backend that will do the full configuration load. +// +// Although this method returns only the backend configuration, at present it +// actually loads and validates the entire configuration first. Therefore errors +// returned may be about other aspects of the configuration. This behavior may +// change in future, so callers must not rely on it. (That is, they must expect +// that a call to loadSingleModule or loadConfig could fail on the same +// directory even if loadBackendConfig succeeded.) +func (m *Meta) loadBackendConfig(rootDir string) (*configs.Backend, tfdiags.Diagnostics) { + mod, diags := m.loadSingleModule(rootDir) + return mod.Backend, diags +} + // installModules reads a root module from the given directory and attempts // recursively install all of its descendent modules. // diff --git a/helper/schema/backend.go b/helper/schema/backend.go index 57fbba744..6d5fb4be8 100644 --- a/helper/schema/backend.go +++ b/helper/schema/backend.go @@ -3,6 +3,11 @@ package schema import ( "context" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/config/configschema" + "github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/terraform" ) @@ -38,41 +43,50 @@ func FromContextBackendConfig(ctx context.Context) *ResourceData { return ctx.Value(backendConfigKey).(*ResourceData) } -func (b *Backend) Input( - input terraform.UIInput, - c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - if b == nil { - return c, nil - } - - return schemaMap(b.Schema).Input(input, c) +func (b *Backend) ConfigSchema() *configschema.Block { + // This is an alias of CoreConfigSchema just to implement the + // backend.Backend interface. + return b.CoreConfigSchema() } -func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) { - if b == nil { - return nil, nil - } - - return schemaMap(b.Schema).Validate(c) -} - -func (b *Backend) Configure(c *terraform.ResourceConfig) error { +func (b *Backend) ValidateConfig(obj cty.Value) tfdiags.Diagnostics { if b == nil { return nil } + var diags tfdiags.Diagnostics + shimRC := b.shimConfig(obj) + warns, errs := schemaMap(b.Schema).Validate(shimRC) + for _, warn := range warns { + diags = diags.Append(tfdiags.SimpleWarning(warn)) + } + for _, err := range errs { + diags = diags.Append(err) + } + return diags +} + +func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { + if b == nil { + return nil + } + + var diags tfdiags.Diagnostics sm := schemaMap(b.Schema) + shimRC := b.shimConfig(obj) // Get a ResourceData for this configuration. To do this, we actually // generate an intermediary "diff" although that is never exposed. - diff, err := sm.Diff(nil, c, nil, nil) + diff, err := sm.Diff(nil, shimRC, nil, nil) if err != nil { - return err + diags = diags.Append(err) + return diags } data, err := sm.Data(nil, diff) if err != nil { - return err + diags = diags.Append(err) + return diags } b.config = data @@ -80,11 +94,24 @@ func (b *Backend) Configure(c *terraform.ResourceConfig) error { err = b.ConfigureFunc(context.WithValue( context.Background(), backendConfigKey, data)) if err != nil { - return err + diags = diags.Append(err) + return diags } } - return nil + return diags +} + +// shimConfig turns a new-style cty.Value configuration (which must be of +// an object type) into a minimal old-style *terraform.ResourceConfig object +// that should be populated enough to appease the not-yet-updated functionality +// in this package. This should be removed once everything is updated. +func (b *Backend) shimConfig(obj cty.Value) *terraform.ResourceConfig { + shimMap := hcl2shim.ConfigValueFromHCL2(obj).(map[string]interface{}) + return &terraform.ResourceConfig{ + Config: shimMap, + Raw: shimMap, + } } // Config returns the configuration. This is available after Configure is diff --git a/helper/schema/backend_test.go b/helper/schema/backend_test.go index aae89cbf4..85ef6408c 100644 --- a/helper/schema/backend_test.go +++ b/helper/schema/backend_test.go @@ -5,15 +5,14 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/terraform" + "github.com/zclconf/go-cty/cty" ) func TestBackendValidate(t *testing.T) { cases := []struct { Name string B *Backend - Config map[string]interface{} + Config map[string]cty.Value Err bool }{ { @@ -26,7 +25,7 @@ func TestBackendValidate(t *testing.T) { }, }, }, - nil, + map[string]cty.Value{}, true, }, @@ -40,8 +39,8 @@ func TestBackendValidate(t *testing.T) { }, }, }, - map[string]interface{}{ - "foo": "bar", + map[string]cty.Value{ + "foo": cty.StringVal("bar"), }, false, }, @@ -49,14 +48,9 @@ func TestBackendValidate(t *testing.T) { for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c, err := config.NewRawConfig(tc.Config) - if err != nil { - t.Fatalf("err: %s", err) - } - - _, es := tc.B.Validate(terraform.NewResourceConfig(c)) - if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v", i, es) + diags := tc.B.ValidateConfig(cty.ObjectVal(tc.Config)) + if diags.HasErrors() != tc.Err { + t.Errorf("wrong number of diagnostics") } }) } @@ -66,7 +60,7 @@ func TestBackendConfigure(t *testing.T) { cases := []struct { Name string B *Backend - Config map[string]interface{} + Config map[string]cty.Value Err bool }{ { @@ -88,8 +82,8 @@ func TestBackendConfigure(t *testing.T) { return nil }, }, - map[string]interface{}{ - "foo": 42, + map[string]cty.Value{ + "foo": cty.NumberIntVal(42), }, false, }, @@ -97,14 +91,9 @@ func TestBackendConfigure(t *testing.T) { for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c, err := config.NewRawConfig(tc.Config) - if err != nil { - t.Fatalf("err: %s", err) - } - - err = tc.B.Configure(terraform.NewResourceConfig(c)) - if err != nil != tc.Err { - t.Fatalf("%d: %s", i, err) + diags := tc.B.Configure(cty.ObjectVal(tc.Config)) + if diags.HasErrors() != tc.Err { + t.Errorf("wrong number of diagnostics") } }) }