diff --git a/backend/backend.go b/backend/backend.go index 414f60c81..4cec8a868 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -275,3 +275,7 @@ const ( // performed at all. OperationFailure OperationResult = 1 ) + +func (r OperationResult) ExitStatus() int { + return int(r) +} diff --git a/command/apply.go b/command/apply.go index 14baa5aa7..42c402565 100644 --- a/command/apply.go +++ b/command/apply.go @@ -7,12 +7,13 @@ import ( "sort" "strings" + "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/go-getter" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" ) @@ -118,66 +119,75 @@ func (c *ApplyCommand) Run(args []string) int { var diags tfdiags.Diagnostics - // Load the module if we don't have one yet (not running from plan) - var mod *module.Tree + var backendConfig *configs.Backend if plan == nil { - var modDiags tfdiags.Diagnostics - mod, modDiags = c.Module(configPath) - diags = diags.Append(modDiags) - if modDiags.HasErrors() { + var configDiags tfdiags.Diagnostics + backendConfig, configDiags = c.loadBackendConfig(configPath) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { c.showDiagnostics(diags) return 1 } } - var conf *config.Config - if mod != nil { - conf = mod.Config() - } - // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, + b, beDiags := c.Backend(&BackendOpts{ + Config: backendConfig, Plan: plan, }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(beDiags) + if beDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } + // Before we delegate to the backend, we'll print any warning diagnostics + // we've accumulated here, since the backend will start fresh with its own + // diagnostics. + c.showDiagnostics(diags) + diags = nil + // Build the operation opReq := c.Operation() opReq.AutoApprove = autoApprove opReq.Destroy = c.Destroy - opReq.DestroyForce = destroyForce - opReq.Module = mod + opReq.ConfigDir = configPath opReq.Plan = plan opReq.PlanRefresh = refresh opReq.Type = backend.OperationTypeApply - - op, err := c.RunOperation(b, opReq) + opReq.AutoApprove = autoApprove + opReq.DestroyForce = destroyForce + opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { - diags = diags.Append(err) - } - - c.showDiagnostics(diags) - if diags.HasErrors() { + c.showDiagnostics(err) return 1 } - if !c.Destroy { - // Get the right module that we used. If we ran a plan, then use - // that module. - if plan != nil { - mod = plan.Module - } - - if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" { - c.Ui.Output(c.Colorize().Color(outputs)) - } + op, err := c.RunOperation(b, opReq) + if err != nil { + c.showDiagnostics(err) + return 1 + } + if op.Result != backend.OperationSuccess { + return op.Result.ExitStatus() } - return op.ExitCode + if !c.Destroy { + // TODO: Print outputs, once this is updated to use new config types. + /* + // Get the right module that we used. If we ran a plan, then use + // that module. + if plan != nil { + mod = plan.Module + } + + if outputs := outputsAsString(op.State, terraform.RootModulePath, mod.Config().Outputs, true); outputs != "" { + c.Ui.Output(c.Colorize().Color(outputs)) + } + */ + } + + return op.Result.ExitStatus() } func (c *ApplyCommand) Help() string { diff --git a/command/autocomplete.go b/command/autocomplete.go index 1ce90e4c9..82d6a4be2 100644 --- a/command/autocomplete.go +++ b/command/autocomplete.go @@ -49,15 +49,15 @@ func (m *Meta) completePredictWorkspaceName() complete.Predictor { return nil } - cfg, err := m.Config(configPath) - if err != nil { + backendConfig, diags := m.loadBackendConfig(configPath) + if diags.HasErrors() { return nil } - b, err := m.Backend(&BackendOpts{ - Config: cfg, + b, diags := m.Backend(&BackendOpts{ + Config: backendConfig, }) - if err != nil { + if diags.HasErrors() { return nil } diff --git a/command/command_test.go b/command/command_test.go index 79330f42b..d0284480f 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -552,9 +552,9 @@ func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State state := terraform.NewState() state.Backend = &terraform.BackendState{ - Type: "http", - Config: map[string]interface{}{"address": srv.URL}, - Hash: 2529831861221416334, + Type: "http", + ConfigRaw: json.RawMessage(fmt.Sprintf(`{"address":%q}`, srv.URL)), + Hash: 2529831861221416334, } return state, srv diff --git a/command/console.go b/command/console.go index f8537f3fc..1c53ef659 100644 --- a/command/console.go +++ b/command/console.go @@ -2,11 +2,9 @@ package command import ( "bufio" - "fmt" "strings" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/helper/wrappedstreams" "github.com/hashicorp/terraform/repl" "github.com/hashicorp/terraform/tfdiags" @@ -41,43 +39,46 @@ func (c *ConsoleCommand) Run(args []string) int { var diags tfdiags.Diagnostics - // Load the module - mod, diags := c.Module(configPath) + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) if diags.HasErrors() { c.showDiagnostics(diags) return 1 } - var conf *config.Config - if mod != nil { - conf = mod.Config() - } - // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) - - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } // We require a local backend local, ok := b.(backend.Local) if !ok { + c.showDiagnostics(diags) // in case of any warnings in here c.Ui.Error(ErrUnsupportedLocalOp) return 1 } // Build the operation opReq := c.Operation() - opReq.Module = mod + opReq.ConfigDir = configPath + opReq.ConfigLoader, err = c.initConfigLoader() + if err != nil { + diags = diags.Append(err) + c.showDiagnostics(diags) + return 1 + } // Get the context - ctx, _, err := local.Context(opReq) - if err != nil { - c.Ui.Error(err.Error()) + ctx, _, ctxDiags := local.Context(opReq) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/graph.go b/command/graph.go index 6b9a0c524..1497c0bcd 100644 --- a/command/graph.go +++ b/command/graph.go @@ -8,8 +8,6 @@ import ( "github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/terraform" ) @@ -60,55 +58,47 @@ func (c *GraphCommand) Run(args []string) int { var diags tfdiags.Diagnostics - // Load the module - var mod *module.Tree - if plan == nil { - var modDiags tfdiags.Diagnostics - mod, modDiags = c.Module(configPath) - diags = diags.Append(modDiags) - if modDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - } - - var conf *config.Config - if mod != nil { - conf = mod.Config() + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, - Plan: plan, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } // We require a local backend local, ok := b.(backend.Local) if !ok { + c.showDiagnostics(diags) // in case of any warnings in here c.Ui.Error(ErrUnsupportedLocalOp) return 1 } - // Building a graph may require config module to be present, even if it's - // empty. - if mod == nil && plan == nil { - mod = module.NewEmptyTree() - } - // Build the operation opReq := c.Operation() - opReq.Module = mod + opReq.ConfigDir = configPath + opReq.ConfigLoader, err = c.initConfigLoader() opReq.Plan = plan + if err != nil { + diags = diags.Append(err) + c.showDiagnostics(diags) + return 1 + } // Get the context - ctx, _, err := local.Context(opReq) - if err != nil { - c.Ui.Error(err.Error()) + ctx, _, ctxDiags := local.Context(opReq) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/import.go b/command/import.go index b19fa3293..479259fd8 100644 --- a/command/import.go +++ b/command/import.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) @@ -76,37 +76,34 @@ func (c *ImportCommand) Run(args []string) int { var diags tfdiags.Diagnostics - // Load the module - var mod *module.Tree - if configPath != "" { - if empty, _ := config.IsEmptyDir(configPath); empty { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "No Terraform configuration files", - Detail: fmt.Sprintf( - "The directory %s does not contain any Terraform configuration files (.tf or .tf.json). To specify a different configuration directory, use the -config=\"...\" command line option.", - configPath, - ), - }) - c.showDiagnostics(diags) - return 1 - } + if !c.dirIsConfigPath(configPath) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No Terraform configuration files", + Detail: fmt.Sprintf( + "The directory %s does not contain any Terraform configuration files (.tf or .tf.json). To specify a different configuration directory, use the -config=\"...\" command line option.", + configPath, + ), + }) + c.showDiagnostics(diags) + return 1 + } - var modDiags tfdiags.Diagnostics - mod, modDiags = c.Module(configPath) - diags = diags.Append(modDiags) - if modDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } + // Load the full config, so we can verify that the target resource is + // already configured. + config, configDiags := c.loadConfig(configPath) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 } // Verify that the given address points to something that exists in config. // This is to reduce the risk that a typo in the resource address will // import something that Terraform will want to immediately destroy on // the next plan, and generally acts as a reassurance of user intent. - targetMod := mod.Child(addr.Path) - if targetMod == nil { + targetConfig := config.Descendent(addr.Path) + if targetConfig == nil { modulePath := addr.WholeModuleAddress().String() diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -119,10 +116,11 @@ func (c *ImportCommand) Run(args []string) int { c.showDiagnostics(diags) return 1 } - rcs := targetMod.Config().Resources - var rc *config.Resource + targetMod := targetConfig.Module + rcs := targetMod.ManagedResources + var rc *configs.ManagedResource for _, thisRc := range rcs { - if addr.MatchesConfig(targetMod, thisRc) { + if addr.MatchesManagedResourceConfig(addr.Path, thisRc) { rc = thisRc break } @@ -154,11 +152,12 @@ func (c *ImportCommand) Run(args []string) int { } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: mod.Config(), + b, backendDiags := c.Backend(&BackendOpts{ + Config: config.Module.Backend, }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } @@ -175,12 +174,19 @@ func (c *ImportCommand) Run(args []string) int { // Build the operation opReq := c.Operation() - opReq.Module = mod + opReq.ConfigDir = configPath + opReq.ConfigLoader, err = c.initConfigLoader() + if err != nil { + diags = diags.Append(err) + c.showDiagnostics(diags) + return 1 + } // Get the context - ctx, state, err := local.Context(opReq) - if err != nil { - c.Ui.Error(err.Error()) + ctx, state, ctxDiags := local.Context(opReq) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/init.go b/command/init.go index ecc459091..e87387cec 100644 --- a/command/init.go +++ b/command/init.go @@ -7,16 +7,19 @@ import ( "sort" "strings" - "github.com/posener/complete" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/terraform/backend" + backendinit "github.com/hashicorp/terraform/backend/init" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/helper/variables" + "github.com/hashicorp/terraform/config/configschema" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" + "github.com/posener/complete" + "github.com/zclconf/go-cty/cty" ) // InitCommand is a Command implementation that takes a Terraform @@ -37,9 +40,9 @@ type InitCommand struct { func (c *InitCommand) Run(args []string) int { var flagFromModule string var flagBackend, flagGet, flagUpgrade bool - var flagConfigExtra map[string]interface{} var flagPluginPath FlagStringSlice var flagVerifyPlugins bool + flagConfigExtra := newRawFlags("-backend-config") args, err := c.Meta.process(args, false) if err != nil { @@ -47,7 +50,7 @@ func (c *InitCommand) Run(args []string) int { } cmdFlags := c.flagSet("init") cmdFlags.BoolVar(&flagBackend, "backend", true, "") - cmdFlags.Var((*variables.FlagAny)(&flagConfigExtra), "backend-config", "") + cmdFlags.Var(flagConfigExtra, "backend-config", "") cmdFlags.StringVar(&flagFromModule, "from-module", "", "copy the source of the given module into the directory before init") cmdFlags.BoolVar(&flagGet, "get", true, "") cmdFlags.BoolVar(&c.getPlugins, "get-plugins", true, "") @@ -64,6 +67,8 @@ func (c *InitCommand) Run(args []string) int { return 1 } + var diags tfdiags.Diagnostics + if len(flagPluginPath) > 0 { c.pluginPath = flagPluginPath c.getPlugins = false @@ -129,18 +134,23 @@ func (c *InitCommand) Run(args []string) int { ))) header = true - s := module.NewStorage("", c.Services) - if err := s.GetModule(path, src); err != nil { - c.Ui.Error(fmt.Sprintf("Error copying source module: %s", err)) + hooks := uiModuleInstallHooks{ + Ui: c.Ui, + ShowLocalPaths: false, // since they are in a weird location for init + } + + initDiags := c.initDirFromModule(path, src, hooks) + diags = diags.Append(initDiags) + if initDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } } // If our directory is empty, then we're done. We can't get or setup // the backend with an empty directory. - empty, err := config.IsEmptyDir(path) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error checking configuration: %s", err)) + if empty, err := config.IsEmptyDir(path); err != nil { + diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err)) return 1 } if empty { @@ -153,32 +163,34 @@ func (c *InitCommand) Run(args []string) int { // If we're performing a get or loading the backend, then we perform // some extra tasks. if flagGet || flagBackend { - conf, err := c.Config(path) - if err != nil { + config, confDiags := c.loadSingleModule(path) + diags = diags.Append(confDiags) + if confDiags.HasErrors() { // Since this may be the user's first ever interaction with Terraform, // we'll provide some additional context in this case. c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(err) + c.showDiagnostics(diags) return 1 } // If we requested downloading modules and have modules in the config - if flagGet && len(conf.Modules) > 0 { + if flagGet && len(config.ModuleCalls) > 0 { header = true - getMode := module.GetModeGet if flagUpgrade { - getMode = module.GetModeUpdate - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold]Upgrading modules..."))) + c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Upgrading modules..."))) } else { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold]Initializing modules..."))) + c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[reset][bold]Initializing modules..."))) } - if err := getModules(&c.Meta, path, getMode); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error downloading modules: %s", err)) + hooks := uiModuleInstallHooks{ + Ui: c.Ui, + ShowLocalPaths: true, + } + instDiags := c.installModules(path, flagUpgrade, hooks) + diags = diags.Append(instDiags) + if instDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } } @@ -188,21 +200,52 @@ func (c *InitCommand) Run(args []string) int { if flagBackend { header = true + var backendSchema *configschema.Block + // Only output that we're initializing a backend if we have // something in the config. We can be UNSETTING a backend as well // in which case we choose not to show this. - if conf.Terraform != nil && conf.Terraform.Backend != nil { - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "\n[reset][bold]Initializing the backend..."))) + if config.Backend != nil { + c.Ui.Output(c.Colorize().Color(fmt.Sprintf("\n[reset][bold]Initializing the backend..."))) + + backendType := config.Backend.Type + bf := backendinit.Backend(backendType) + if bf == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported backend type", + Detail: fmt.Sprintf("There is no backend type named %q.", backendType), + Subject: &config.Backend.TypeRange, + }) + c.showDiagnostics() + return 1 + } + + b := bf() + backendSchema = b.ConfigSchema() + } + + var backendConfigOverride hcl.Body + if backendSchema != nil { + var overrideDiags tfdiags.Diagnostics + backendConfigOverride, overrideDiags = c.backendConfigOverrideBody(flagConfigExtra, backendSchema) + diags = diags.Append(overrideDiags) + if overrideDiags.HasErrors() { + c.showDiagnostics() + return 1 + } } opts := &BackendOpts{ - Config: conf, - ConfigExtra: flagConfigExtra, - Init: true, + Config: config.Backend, + ConfigOverride: backendConfigOverride, + Init: true, } - if back, err = c.Backend(opts); err != nil { - c.Ui.Error(err.Error()) + var backDiags tfdiags.Diagnostics + back, backDiags = c.Backend(opts) + diags = diags.Append(backDiags) + if backDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } } @@ -213,8 +256,9 @@ func (c *InitCommand) Run(args []string) int { // instantiate one. This might fail if it wasn't already initalized // by a previous run, so we must still expect that "back" may be nil // in code that follows. - back, err = c.Backend(nil) - if err != nil { + var backDiags tfdiags.Diagnostics + back, backDiags = c.Backend(nil) + if backDiags.HasErrors() { // This is fine. We'll proceed with no backend, then. back = nil } @@ -269,6 +313,75 @@ func (c *InitCommand) Run(args []string) int { return 0 } +// backendConfigOverrideBody interprets the raw values of -backend-config +// arguments into a hcl Body that should override the backend settings given +// in the configuration. +// +// If the result is nil then no override needs to be provided. +// +// If the returned diagnostics contains errors then the returned body may be +// incomplete or invalid. +func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { + items := flags.AllItems() + if len(items) == 0 { + return nil, nil + } + + var ret hcl.Body + var diags tfdiags.Diagnostics + synthVals := make(map[string]cty.Value) + + mergeBody := func(newBody hcl.Body) { + if ret == nil { + ret = newBody + } else { + ret = configs.MergeBodies(ret, newBody) + } + } + flushVals := func() { + if len(synthVals) == 0 { + return + } + newBody := configs.SynthBody("-backend-config=...", synthVals) + mergeBody(newBody) + synthVals = make(map[string]cty.Value) + } + + for _, item := range items { + eq := strings.Index(item.Value, "=") + + if eq == -1 { + // The value is interpreted as a filename. + newBody, fileDiags := c.loadHCLFile(item.Value) + diags = diags.Append(fileDiags) + flushVals() // deal with any accumulated individual values first + mergeBody(newBody) + } else { + name := item.Value[:eq] + rawValue := item.Value[eq+1:] + attrS := schema.Attributes[name] + if attrS == nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid backend configuration argument", + fmt.Sprintf("The backend configuration argument %q given on the command line is not expected for the selected backend type.", name), + )) + continue + } + value, valueDiags := configValueFromCLI(item.String(), rawValue, attrS.Type) + diags = diags.Append(valueDiags) + if valueDiags.HasErrors() { + continue + } + synthVals[name] = value + } + } + + flushVals() + + return ret, diags +} + // Load the complete module tree, and fetch any missing providers. // This method outputs its own Ui. func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade bool) error { diff --git a/command/init_test.go b/command/init_test.go index 32118b486..add530bbb 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -11,6 +11,9 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/configs" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend/local" "github.com/hashicorp/terraform/helper/copy" "github.com/hashicorp/terraform/plugin/discovery" @@ -312,8 +315,8 @@ func TestInit_backendConfigFile(t *testing.T) { // Read our saved backend config and verify we have our settings state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) - if v := state.Backend.Config["path"]; v != "hello" { - t.Fatalf("bad: %#v", v) + if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { + t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } } @@ -344,8 +347,8 @@ func TestInit_backendConfigFileChange(t *testing.T) { // Read our saved backend config and verify we have our settings state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) - if v := state.Backend.Config["path"]; v != "hello" { - t.Fatalf("bad: %#v", v) + if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { + t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } } @@ -371,8 +374,8 @@ func TestInit_backendConfigKV(t *testing.T) { // Read our saved backend config and verify we have our settings state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) - if v := state.Backend.Config["path"]; v != "hello" { - t.Fatalf("bad: %#v", v) + if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { + t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } } @@ -419,11 +422,13 @@ func TestInit_backendReinitWithExtra(t *testing.T) { m := testMetaBackend(t, nil) opts := &BackendOpts{ - ConfigExtra: map[string]interface{}{"path": "hello"}, - Init: true, + ConfigOverride: configs.SynthBody("synth", map[string]cty.Value{ + "path": cty.StringVal("hello"), + }), + Init: true, } - b, err := m.backendConfig(opts) + _, cHash, err := m.backendConfig(opts) if err != nil { t.Fatal(err) } @@ -443,28 +448,23 @@ func TestInit_backendReinitWithExtra(t *testing.T) { // Read our saved backend config and verify we have our settings state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) - if v := state.Backend.Config["path"]; v != "hello" { - t.Fatalf("bad: %#v", v) + if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { + t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } - if state.Backend.Hash != b.Hash { + if state.Backend.Hash != cHash { t.Fatal("mismatched state and config backend hashes") } - if state.Backend.Rehash() != b.Rehash() { - t.Fatal("mismatched state and config re-hashes") - } - // init again and make sure nothing changes if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } state = testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) - if v := state.Backend.Config["path"]; v != "hello" { - t.Fatalf("bad: %#v", v) + if got, want := string(state.Backend.ConfigRaw), `{"path":"hello"}`; got != want { + t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } - - if state.Backend.Hash != b.Hash { + if state.Backend.Hash != cHash { t.Fatal("mismatched state and config backend hashes") } } @@ -490,8 +490,8 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { // Read our saved backend config and verify we have our settings state := testStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) - if v := state.Backend.Config["path"]; v != "foo" { - t.Fatalf("bad: %#v", v) + if got, want := string(state.Backend.ConfigRaw), `{"path":"foo"}`; got != want { + t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) } backendHash := state.Backend.Hash diff --git a/command/meta.go b/command/meta.go index 60450bcd6..c4b3830b2 100644 --- a/command/meta.go +++ b/command/meta.go @@ -261,6 +261,15 @@ func (m *Meta) StdinPiped() bool { return fi.Mode()&os.ModeNamedPipe != 0 } +// RunOperation executes the given operation on the given backend, blocking +// until that operation completes or is inteerrupted, and then returns +// the RunningOperation object representing the completed or +// aborted operation that is, despite the name, no longer running. +// +// An error is returned if the operation either fails to start or is cancelled. +// If the operation runs to completion then no error is returned even if the +// operation itself is unsuccessful. Use the "Result" field of the +// returned operation object to recognize operation-level failure. func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) { op, err := b.Operation(context.Background(), opReq) if err != nil { @@ -302,10 +311,6 @@ func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*back // operation completed normally } - if op.Err != nil { - return op, op.Err - } - return op, nil } diff --git a/command/meta_backend.go b/command/meta_backend.go index e87c6d551..7f8ab6a78 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -5,25 +5,26 @@ package command import ( "context" + "encoding/json" "errors" "fmt" - "io/ioutil" "log" "path/filepath" "strings" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/hcl" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcldec" "github.com/hashicorp/terraform/backend" + backendinit "github.com/hashicorp/terraform/backend/init" + backendlocal "github.com/hashicorp/terraform/backend/local" "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" - - backendInit "github.com/hashicorp/terraform/backend/init" - backendLocal "github.com/hashicorp/terraform/backend/local" + "github.com/hashicorp/terraform/tfdiags" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" ) // BackendOpts are the options used to initialize a backend.Backend. @@ -32,14 +33,10 @@ type BackendOpts struct { // 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 - // from a file. - ConfigFile string - - // ConfigExtra is extra configuration to merge into the backend - // configuration after the extra file above. - ConfigExtra map[string]interface{} + // ConfigOverride is an hcl.Body that, if non-nil, will be used with + // configs.MergeBodies to override the type-specific backend configuration + // arguments in Config. + ConfigOverride hcl.Body // Plan is a plan that is being used. If this is set, the backend // configuration and output configuration will come from this plan. @@ -69,7 +66,9 @@ type BackendOpts struct { // and is unsafe to create multiple backends used at once. This function // can be called multiple times with each backend being "live" (usable) // one at a time. -func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { +func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + // If no opts are set, then initialize if opts == nil { opts = &BackendOpts{} @@ -79,17 +78,20 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { // local operation. var b backend.Backend if !opts.ForceLocal { - var err error - // If we have a plan then, we get the the backend from there. Otherwise, // the backend comes from the configuration. if opts.Plan != nil { - b, err = m.backendFromPlan(opts) + var backendDiags tfdiags.Diagnostics + b, backendDiags = m.backendFromPlan(opts) + diags = diags.Append(backendDiags) } else { - b, err = m.backendFromConfig(opts) + var backendDiags tfdiags.Diagnostics + b, backendDiags = m.backendFromConfig(opts) + diags = diags.Append(backendDiags) } - if err != nil { - return nil, err + + if diags.HasErrors() { + return nil, diags } log.Printf("[INFO] command: backend initialized: %T", b) @@ -99,6 +101,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { cliOpts := &backend.CLIOpts{ CLI: m.Ui, CLIColor: m.Colorize(), + ShowDiagnostics: m.showDiagnostics, StatePath: m.statePath, StateOutPath: m.stateOutPath, StateBackupPath: m.backupPath, @@ -115,10 +118,12 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { // If the backend supports CLI initialization, do it. if cli, ok := b.(backend.CLI); ok { if err := cli.CLIInit(cliOpts); err != nil { - return nil, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "Error initializing backend %T: %s\n\n"+ - "This is a bug, please report it to the backend developer", - b, err) + "This is a bug; please report it to the backend developer", + b, err, + )) + return nil, diags } } @@ -180,17 +185,19 @@ func (m *Meta) Operation() *backend.Operation { } // backendConfig returns the local configuration for the backend -func (m *Meta) backendConfig(opts *BackendOpts) (*config.Backend, error) { +func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if opts.Config == nil { // check if the config was missing, or just not required - conf, err := m.Config(".") + conf, err := m.loadBackendConfig(".") if err != nil { - return nil, err + return nil, 0, err } if conf == nil { log.Println("[INFO] command: no config, returning nil") - return nil, nil + return nil, 0, nil } log.Println("[WARN] BackendOpts.Config not set, but config found") @@ -199,81 +206,37 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*config.Backend, error) { c := opts.Config - // If there is no Terraform configuration block, no backend config - if c.Terraform == nil { - log.Println("[INFO] command: empty terraform config, returning nil") - return nil, nil + if c == nil { + log.Println("[INFO] command: no explicit backend config") + return nil, 0, nil } - // Get the configuration for the backend itself. - backend := c.Terraform.Backend - if backend == nil { - log.Println("[INFO] command: empty backend config, returning nil") - return nil, nil + bf := backendinit.Backend(c.Type) + if bf == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid backend type", + Detail: fmt.Sprintf("There is no backend type named %q.", c.Type), + Subject: &c.TypeRange, + }) + return nil, 0, diags + } + b := bf() + + configSchema := b.ConfigSchema() + configBody := c.Config + configHash := c.Hash(configSchema) + + // If we have an override configuration body then we must apply it now. + if opts.ConfigOverride != nil { + configBody = configs.MergeBodies(configBody, opts.ConfigOverride) } - // If we have a config file set, load that and merge. - if opts.ConfigFile != "" { - log.Printf( - "[DEBUG] command: loading extra backend config from: %s", - opts.ConfigFile) - rc, err := m.backendConfigFile(opts.ConfigFile) - if err != nil { - return nil, fmt.Errorf( - "Error loading extra configuration file for backend: %s", err) - } - - // Merge in the configuration - backend.RawConfig = backend.RawConfig.Merge(rc) - } - - // If we have extra config values, merge that - if len(opts.ConfigExtra) > 0 { - log.Printf( - "[DEBUG] command: adding extra backend config from CLI") - rc, err := config.NewRawConfig(opts.ConfigExtra) - if err != nil { - return nil, fmt.Errorf( - "Error adding extra backend configuration from CLI: %s", err) - } - - // Merge in the configuration - backend.RawConfig = backend.RawConfig.Merge(rc) - } - - // Validate the backend early. We have to do this before the normal - // config validation pass since backend loading happens earlier. - if errs := backend.Validate(); len(errs) > 0 { - return nil, multierror.Append(nil, errs...) - } - - // Return the configuration which may or may not be set - return backend, nil -} - -// backendConfigFile loads the extra configuration to merge with the -// backend configuration from an extra file if specified by -// BackendOpts.ConfigFile. -func (m *Meta) backendConfigFile(path string) (*config.RawConfig, error) { - // Read the file - d, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - // Parse it - hclRoot, err := hcl.Parse(string(d)) - if err != nil { - return nil, err - } - - // Decode it - var c map[string]interface{} - if err := hcl.DecodeObject(&c, hclRoot); err != nil { - return nil, err - } - - return config.NewRawConfig(c) + // We'll shallow-copy configs.Backend here so that we can replace the + // body without affecting others that hold this reference. + configCopy := *c + c.Config = configBody + return &configCopy, configHash, diags } // backendFromConfig returns the initialized (not configured) backend @@ -285,21 +248,11 @@ func (m *Meta) backendConfigFile(path string) (*config.RawConfig, error) { // // This function may query the user for input unless input is disabled, in // which case this function will error. -func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { +func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { // Get the local backend configuration. - c, err := m.backendConfig(opts) - if err != nil { - return nil, fmt.Errorf("Error loading backend config: %s", err) - } - - // cHash defaults to zero unless c is set - var cHash uint64 - if c != nil { - // We need to rehash to get the value since we may have merged the - // config with an extra ConfigFile. We don't do this when merging - // because we do want the ORIGINAL value on c so that we store - // that to not detect drift. This is covered in tests. - cHash = c.Rehash() + c, cHash, diags := m.backendConfig(opts) + if diags.HasErrors() { + return nil, diags } // Get the path to where we store a local cache of backend configuration @@ -308,7 +261,8 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { statePath := filepath.Join(m.DataDir(), DefaultStateFilename) sMgr := &state.LocalState{Path: statePath} if err := sMgr.RefreshState(); err != nil { - return nil, fmt.Errorf("Error loading state: %s", err) + diags = diags.Append(fmt.Errorf("Failed to load state: %s", err)) + return nil, diags } // Load the state, it must be non-nil for the tests below but can be empty @@ -349,10 +303,11 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { "Unsetting the previously set backend %q", s.Backend.Type) m.backendInitRequired(initReason) - return nil, errBackendInitRequired + diags = diags.Append(errBackendInitRequired) + return nil, diags } - return m.backend_c_r_S(c, sMgr, true) + return m.backend_c_r_S(c, cHash, sMgr, true) // We have a legacy remote state configuration but no new backend config case c == nil && !s.Remote.Empty() && s.Backend.Empty(): @@ -370,10 +325,11 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { "Unsetting the previously set backend %q", s.Backend.Type) m.backendInitRequired(initReason) - return nil, errBackendInitRequired + diags = diags.Append(errBackendInitRequired) + return nil, diags } - return m.backend_c_R_S(c, sMgr) + return m.backend_c_R_S(c, cHash, sMgr) // Configuring a backend for the first time. case c != nil && s.Remote.Empty() && s.Backend.Empty(): @@ -382,24 +338,20 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { "Initial configuration of the requested backend %q", c.Type) m.backendInitRequired(initReason) - return nil, errBackendInitRequired + diags = diags.Append(errBackendInitRequired) + return nil, diags } - return m.backend_C_r_s(c, sMgr) + return m.backend_C_r_s(c, cHash, sMgr) // Potentially changing a backend configuration case c != nil && s.Remote.Empty() && !s.Backend.Empty(): // If our configuration is the same, then we're just initializing // a previously configured remote backend. if !s.Backend.Empty() { - hash := s.Backend.Hash - // on init we need an updated hash containing any extra options - // that were added after merging. - if opts.Init { - hash = s.Backend.Rehash() - } - if hash == cHash { - return m.backend_C_r_S_unchanged(c, sMgr) + storedHash := s.Backend.Hash + if storedHash == cHash { + return m.backend_C_r_S_unchanged(c, cHash, sMgr) } } @@ -408,13 +360,14 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { "Backend configuration changed for %q", c.Type) m.backendInitRequired(initReason) - return nil, errBackendInitRequired + diags = diags.Append(errBackendInitRequired) + return nil, diags } log.Printf( "[WARN] command: backend config change! saved: %d, new: %d", s.Backend.Hash, cHash) - return m.backend_C_r_S_changed(c, sMgr, true) + return m.backend_C_r_S_changed(c, cHash, sMgr, true) // Configuring a backend for the first time while having legacy // remote state. This is very possible if a Terraform user configures @@ -425,7 +378,8 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { "Initial configuration for backend %q", c.Type) m.backendInitRequired(initReason) - return nil, errBackendInitRequired + diags = diags.Append(errBackendInitRequired) + return nil, diags } return m.backend_C_R_s(c, sMgr) @@ -435,17 +389,15 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { case c != nil && !s.Remote.Empty() && !s.Backend.Empty(): // If the hashes are the same, we have a legacy remote state with // an unchanged stored backend state. - hash := s.Backend.Hash - if opts.Init { - hash = s.Backend.Rehash() - } - if hash == cHash { + storedHash := s.Backend.Hash + if storedHash == cHash { if !opts.Init { initReason := fmt.Sprintf( "Legacy remote state found with configured backend %q", c.Type) m.backendInitRequired(initReason) - return nil, errBackendInitRequired + diags = diags.Append(errBackendInitRequired) + return nil, diags } return m.backend_C_R_S_unchanged(c, sMgr, true) @@ -456,7 +408,8 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { "Reconfiguring the backend %q", c.Type) m.backendInitRequired(initReason) - return nil, errBackendInitRequired + diags = diags.Append(errBackendInitRequired) + return nil, diags } // We have change in all three @@ -465,29 +418,33 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, error) { // This should be impossible since all state possibilties are // tested above, but we need a default case anyways and we should // protect against the scenario where a case is somehow removed. - return nil, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "Unhandled backend configuration state. This is a bug. Please\n"+ "report this error with the following information.\n\n"+ "Config Nil: %v\n"+ "Saved Backend Empty: %v\n"+ "Legacy Remote Empty: %v\n", - c == nil, s.Backend.Empty(), s.Remote.Empty()) + c == nil, s.Backend.Empty(), s.Remote.Empty(), + )) + return nil, diags } } // backendFromPlan loads the backend from a given plan file. -func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { - // Precondition check +func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { if opts.Plan == nil { panic("plan should not be nil") } + var diags tfdiags.Diagnostics + // We currently don't allow "-state" to be specified. if m.statePath != "" { - return nil, fmt.Errorf( + diags = diags.Append(fmt.Errorf( "State path cannot be specified with a plan file. The plan itself contains\n" + "the state to use. If you wish to change that, please create a new plan\n" + - "and specify the state path when creating the plan.") + "and specify the state path when creating the plan.", + )) } planBackend := opts.Plan.Backend @@ -503,31 +460,11 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { if !local { // We currently don't allow "-state-out" to be specified. if m.stateOutPath != "" { - return nil, fmt.Errorf(strings.TrimSpace(errBackendPlanStateFlag)) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendPlanStateFlag))) + return nil, diags } } - /* - // Determine the path where we'd be writing state - path := DefaultStateFilename - if !planState.Remote.Empty() || !planBackend.Empty() { - path = filepath.Join(m.DataDir(), DefaultStateFilename) - } - - // If the path exists, then we need to verify we're writing the same - // state lineage. If the path doesn't exist that's okay. - _, err := os.Stat(path) - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("Error checking state destination: %s", err) - } - if err == nil { - // The file exists, we need to read it and compare - if err := m.backendFromPlan_compareStates(state, path); err != nil { - return nil, err - } - } - */ - // If we have a stateOutPath, we must also specify it as the // input path so we can check it properly. We restore it after this // function exits. @@ -536,14 +473,15 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { defer func() { m.statePath = original }() var b backend.Backend - var err error switch { // No remote state at all, all local case planState.Remote.Empty() && planBackend.Empty(): log.Printf("[INFO] command: initializing local backend from plan (not set)") // Get the local backend - b, err = m.Backend(&BackendOpts{ForceLocal: true}) + var backendDiags tfdiags.Diagnostics + b, backendDiags = m.Backend(&BackendOpts{ForceLocal: true}) + diags = diags.Append(backendDiags) // New backend configuration set case planState.Remote.Empty() && !planBackend.Empty(): @@ -551,7 +489,9 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { "[INFO] command: initializing backend from plan: %s", planBackend.Type) - b, err = m.backendInitFromSaved(planBackend) + var backendDiags tfdiags.Diagnostics + b, backendDiags = m.backendInitFromSaved(planBackend) + diags = diags.Append(backendDiags) // Legacy remote state set case !planState.Remote.Empty() && planBackend.Empty(): @@ -565,16 +505,19 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { inmem.WriteState(planState) // Get the backend through the normal means of legacy state - b, err = m.backend_c_R_s(nil, inmem) + var moreDiags tfdiags.Diagnostics + b, moreDiags = m.backend_c_R_s(nil, inmem) + diags = diags.Append(moreDiags) // Both set, this can't happen in a plan. case !planState.Remote.Empty() && !planBackend.Empty(): - return nil, fmt.Errorf(strings.TrimSpace(errBackendPlanBoth)) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendPlanBoth))) + return nil, diags } // If we had an error, return that - if err != nil { - return nil, err + if diags.HasErrors() { + return nil, diags } env := m.Workspace() @@ -582,31 +525,36 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { // Get the state so we can determine the effect of using this plan realMgr, err := b.State(env) if err != nil { - return nil, fmt.Errorf("Error reading state: %s", err) + diags = diags.Append(fmt.Errorf("Error reading state: %s", err)) + return nil, diags } if m.stateLock { stateLocker := clistate.NewLocker(context.Background(), m.stateLockTimeout, m.Ui, m.Colorize()) if err := stateLocker.Lock(realMgr, "backend from plan"); err != nil { - return nil, fmt.Errorf("Error locking state: %s", err) + diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) + return nil, diags } defer stateLocker.Unlock(nil) } if err := realMgr.RefreshState(); err != nil { - return nil, fmt.Errorf("Error reading state: %s", err) + diags = diags.Append(fmt.Errorf("Error reading state: %s", err)) + return nil, diags } real := realMgr.State() if real != nil { // If they're not the same lineage, don't allow this if !real.SameLineage(planState) { - return nil, fmt.Errorf(strings.TrimSpace(errBackendPlanLineageDiff)) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendPlanLineageDiff))) + return nil, diags } // Compare ages comp, err := real.CompareAges(planState) if err != nil { - return nil, fmt.Errorf("Error comparing state ages for safety: %s", err) + diags = diags.Append(fmt.Errorf("Error comparing state ages for safety: %s", err)) + return nil, diags } switch comp { case terraform.StateAgeEqual: @@ -619,15 +567,16 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { // If we have an older serial it is a problem but if we have a // differing serial but are still identical, just let it through. if real.Equal(planState) { - log.Printf( - "[WARN] command: state in plan has older serial, but Equal is true") + log.Printf("[WARN] command: state in plan has older serial, but Equal is true") break } // The real state is newer, this is not allowed. - return nil, fmt.Errorf( + diags = diags.Append(fmt.Errorf( strings.TrimSpace(errBackendPlanOlder), - planState.Serial, real.Serial) + planState.Serial, real.Serial, + )) + return nil, diags } } @@ -640,13 +589,15 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { // realMgr locked above if err := realMgr.WriteState(newState); err != nil { - return nil, fmt.Errorf("Error writing state: %s", err) + diags = diags.Append(fmt.Errorf("Error writing state: %s", err)) + return nil, diags } if err := realMgr.PersistState(); err != nil { - return nil, fmt.Errorf("Error writing state: %s", err) + diags = diags.Append(fmt.Errorf("Error writing state: %s", err)) + return nil, diags } - return b, nil + return b, diags } //------------------------------------------------------------------- @@ -667,8 +618,7 @@ func (m *Meta) backendFromPlan(opts *BackendOpts) (backend.Backend, error) { //------------------------------------------------------------------- // Unconfiguring a backend (moving from backend => local). -func (m *Meta) backend_c_r_S( - c *config.Backend, sMgr state.State, output bool) (backend.Backend, error) { +func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr state.State, output bool) (backend.Backend, tfdiags.Diagnostics) { s := sMgr.State() // Get the backend type for output @@ -677,36 +627,39 @@ func (m *Meta) backend_c_r_S( m.Ui.Output(fmt.Sprintf(strings.TrimSpace(outputBackendMigrateLocal), s.Backend.Type)) // Grab a purely local backend to get the local state if it exists - localB, err := m.Backend(&BackendOpts{ForceLocal: true}) - if err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendLocalRead), err) + localB, diags := m.Backend(&BackendOpts{ForceLocal: true}) + if diags.HasErrors() { + return nil, diags } // Initialize the configured backend - b, err := m.backend_C_r_S_unchanged(c, sMgr) - if err != nil { - return nil, fmt.Errorf( - strings.TrimSpace(errBackendSavedUnsetConfig), s.Backend.Type, err) + b, moreDiags := m.backend_C_r_S_unchanged(c, cHash, sMgr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags } // Perform the migration - err = m.backendMigrateState(&backendMigrateOpts{ + err := m.backendMigrateState(&backendMigrateOpts{ OneType: s.Backend.Type, TwoType: "local", One: b, Two: localB, }) if err != nil { - return nil, err + diags = diags.Append(err) + return nil, diags } // Remove the stored metadata s.Backend = nil if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err)) + return nil, diags } if err := sMgr.PersistState(); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err)) + return nil, diags } if output { @@ -716,219 +669,62 @@ func (m *Meta) backend_c_r_S( } // Return no backend - return nil, nil + return nil, diags } // Legacy remote state -func (m *Meta) backend_c_R_s( - c *config.Backend, sMgr state.State) (backend.Backend, error) { - s := sMgr.State() +func (m *Meta) backend_c_R_s(c *configs.Backend, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - // Warn the user - m.Ui.Warn(strings.TrimSpace(warnBackendLegacy) + "\n") + m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") - // We need to convert the config to map[string]interface{} since that - // is what the backends expect. - var configMap map[string]interface{} - if err := mapstructure.Decode(s.Remote.Config, &configMap); err != nil { - return nil, fmt.Errorf("Error configuring remote state: %s", err) - } - - // Create the config - rawC, err := config.NewRawConfig(configMap) - if err != nil { - return nil, fmt.Errorf("Error configuring remote state: %s", err) - } - config := terraform.NewResourceConfig(rawC) - - // Get the backend - f := backendInit.Backend(s.Remote.Type) - if f == nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendLegacyUnknown), s.Remote.Type) - } - b := f() - - // Configure - if err := b.Configure(config); err != nil { - return nil, fmt.Errorf(errBackendLegacyConfig, err) - } - - return b, nil + diags = diags.Append(fmt.Errorf("Cannot initialize legacy remote state")) + return nil, diags } // Unsetting backend, saved backend, legacy remote state -func (m *Meta) backend_c_R_S( - c *config.Backend, sMgr state.State) (backend.Backend, error) { - // Notify the user - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset]%s\n\n", - strings.TrimSpace(outputBackendUnsetWithLegacy)))) +func (m *Meta) backend_c_R_S(c *configs.Backend, cHash int, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - // Get the backend type for later - backendType := sMgr.State().Backend.Type + m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") - // First, perform the configured => local tranasition - if _, err := m.backend_c_r_S(c, sMgr, false); err != nil { - return nil, err - } - - // Grab a purely local backend - localB, err := m.Backend(&BackendOpts{ForceLocal: true}) - if err != nil { - return nil, fmt.Errorf(errBackendLocalRead, err) - } - - // Grab the state - s := sMgr.State() - - m.Ui.Output(strings.TrimSpace(outputBackendMigrateLegacy)) - // Initialize the legacy backend - oldB, err := m.backendInitFromLegacy(s.Remote) - if err != nil { - return nil, err - } - - // Perform the migration - err = m.backendMigrateState(&backendMigrateOpts{ - OneType: s.Remote.Type, - TwoType: "local", - One: oldB, - Two: localB, - }) - if err != nil { - return nil, err - } - - // Unset the remote state - s = sMgr.State() - if s == nil { - s = terraform.NewState() - } - s.Remote = nil - if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err) - } - if err := sMgr.PersistState(); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err) - } - - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset][green]\n\n"+ - strings.TrimSpace(successBackendUnset), backendType))) - - return nil, nil + diags = diags.Append(fmt.Errorf("Cannot initialize legacy remote state")) + return nil, diags } // Configuring a backend for the first time with legacy remote state. -func (m *Meta) backend_C_R_s( - c *config.Backend, sMgr state.State) (backend.Backend, error) { - // Notify the user - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset]%s\n\n", - strings.TrimSpace(outputBackendConfigureWithLegacy)))) +func (m *Meta) backend_C_R_s(c *configs.Backend, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - // First, configure the new backend - b, err := m.backendInitFromConfig(c) - if err != nil { - return nil, err - } + m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") - // Next, save the new configuration. This will not overwrite our - // legacy remote state. We'll handle that after. - s := sMgr.State() - if s == nil { - s = terraform.NewState() - } - s.Backend = &terraform.BackendState{ - Type: c.Type, - Config: c.RawConfig.Raw, - Hash: c.Hash, - } - if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(errBackendWriteSaved, err) - } - if err := sMgr.PersistState(); err != nil { - return nil, fmt.Errorf(errBackendWriteSaved, err) - } - - // I don't know how this is possible but if we don't have remote - // state config anymore somehow, just return the backend. This - // shouldn't be possible, though. - if s.Remote.Empty() { - return b, nil - } - - m.Ui.Output(strings.TrimSpace(outputBackendMigrateLegacy)) - // Initialize the legacy backend - oldB, err := m.backendInitFromLegacy(s.Remote) - if err != nil { - return nil, err - } - - // Perform the migration - err = m.backendMigrateState(&backendMigrateOpts{ - OneType: s.Remote.Type, - TwoType: c.Type, - One: oldB, - Two: b, - }) - if err != nil { - return nil, err - } - - // Unset the remote state - s = sMgr.State() - if s == nil { - s = terraform.NewState() - } - s.Remote = nil - if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err) - } - if err := sMgr.PersistState(); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err) - } - - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset][green]\n"+strings.TrimSpace(successBackendSet), s.Backend.Type))) - - return b, nil + diags = diags.Append(fmt.Errorf("Cannot initialize legacy remote state")) + return nil, diags } // Configuring a backend for the first time. -func (m *Meta) backend_C_r_s( - c *config.Backend, sMgr state.State) (backend.Backend, error) { +func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { // Get the backend - b, err := m.backendInitFromConfig(c) - if err != nil { - return nil, err + b, configVal, diags := m.backendInitFromConfig(c) + if diags.HasErrors() { + return nil, diags } // Grab a purely local backend to get the local state if it exists - localB, err := m.Backend(&BackendOpts{ForceLocal: true}) - if err != nil { - return nil, fmt.Errorf(errBackendLocalRead, err) + localB, localBDiags := m.Backend(&BackendOpts{ForceLocal: true}) + if localBDiags.HasErrors() { + diags = diags.Append(localBDiags) + return nil, diags } workspaces, err := localB.States() if err != nil { - return nil, fmt.Errorf(errBackendLocalRead, err) + diags = diags.Append(fmt.Errorf(errBackendLocalRead, err)) + return nil, diags } - - var localStates []state.State - for _, workspace := range workspaces { - localState, err := localB.State(workspace) - if err != nil { - return nil, fmt.Errorf(errBackendLocalRead, err) - } - if err := localState.RefreshState(); err != nil { - return nil, fmt.Errorf(errBackendLocalRead, err) - } - - // We only care about non-empty states. - if localS := localState.State(); !localS.Empty() { - localStates = append(localStates, localState) - } + if err := localState.RefreshState(); err != nil { + diags = diags.Append(fmt.Errorf(errBackendLocalRead, err)) + return nil, diags } if len(localStates) > 0 { @@ -940,7 +736,8 @@ func (m *Meta) backend_C_r_s( Two: b, }) if err != nil { - return nil, err + diags = diags.Append(err) + return nil, diags } // we usually remove the local state after migration to prevent @@ -957,14 +754,14 @@ func (m *Meta) backend_C_r_s( } if erase { - for _, localState := range localStates { - // We always delete the local state, unless that was our new state too. - if err := localState.WriteState(nil); err != nil { - return nil, fmt.Errorf(errBackendMigrateLocalDelete, err) - } - if err := localState.PersistState(); err != nil { - return nil, fmt.Errorf(errBackendMigrateLocalDelete, err) - } + // We always delete the local state, unless that was our new state too. + if err := localState.WriteState(nil); err != nil { + diags = diags.Append(fmt.Errorf(errBackendMigrateLocalDelete, err)) + return nil, diags + } + if err := localState.PersistState(); err != nil { + diags = diags.Append(fmt.Errorf(errBackendMigrateLocalDelete, err)) + return nil, diags } } } @@ -972,39 +769,47 @@ func (m *Meta) backend_C_r_s( if m.stateLock { stateLocker := clistate.NewLocker(context.Background(), m.stateLockTimeout, m.Ui, m.Colorize()) if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil { - return nil, fmt.Errorf("Error locking state: %s", err) + diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) + return nil, diags } defer stateLocker.Unlock(nil) } + configJSON, err := ctyjson.Marshal(configVal, b.ConfigSchema().ImpliedType()) + if err != nil { + diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err)) + return nil, diags + } + // Store the metadata in our saved state location s := sMgr.State() if s == nil { s = terraform.NewState() } s.Backend = &terraform.BackendState{ - Type: c.Type, - Config: c.RawConfig.Raw, - Hash: c.Hash, + Type: c.Type, + ConfigRaw: json.RawMessage(configJSON), + Hash: cHash, } if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(errBackendWriteSaved, err) + diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + return nil, diags } if err := sMgr.PersistState(); err != nil { - return nil, fmt.Errorf(errBackendWriteSaved, err) + diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + return nil, diags } m.Ui.Output(m.Colorize().Color(fmt.Sprintf( "[reset][green]\n"+strings.TrimSpace(successBackendSet), s.Backend.Type))) // Return the backend - return b, nil + return b, diags } // Changing a previously saved backend. -func (m *Meta) backend_C_r_S_changed( - c *config.Backend, sMgr state.State, output bool) (backend.Backend, error) { +func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr state.State, output bool) (backend.Backend, tfdiags.Diagnostics) { if output { // Notify the user m.Ui.Output(m.Colorize().Color(fmt.Sprintf( @@ -1016,10 +821,9 @@ func (m *Meta) backend_C_r_S_changed( s := sMgr.State() // Get the backend - b, err := m.backendInitFromConfig(c) - if err != nil { - return nil, fmt.Errorf( - "Error initializing new backend: %s", err) + b, configVal, diags := m.backendInitFromConfig(c) + if diags.HasErrors() { + return nil, diags } // no need to confuse the user if the backend types are the same @@ -1028,47 +832,57 @@ func (m *Meta) backend_C_r_S_changed( } // Grab the existing backend - oldB, err := m.backend_C_r_S_unchanged(c, sMgr) - if err != nil { - return nil, fmt.Errorf( - "Error loading previously configured backend: %s", err) + oldB, oldBDiags := m.backend_C_r_S_unchanged(c, cHash, sMgr) + diags = diags.Append(oldBDiags) + if oldBDiags.HasErrors() { + return nil, diags } // Perform the migration - err = m.backendMigrateState(&backendMigrateOpts{ + err := m.backendMigrateState(&backendMigrateOpts{ OneType: s.Backend.Type, TwoType: c.Type, One: oldB, Two: b, }) if err != nil { - return nil, err + diags = diags.Append(err) + return nil, diags } if m.stateLock { stateLocker := clistate.NewLocker(context.Background(), m.stateLockTimeout, m.Ui, m.Colorize()) if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil { - return nil, fmt.Errorf("Error locking state: %s", err) + diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) + return nil, diags } defer stateLocker.Unlock(nil) } + configJSON, err := ctyjson.Marshal(configVal, b.ConfigSchema().ImpliedType()) + if err != nil { + diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err)) + return nil, diags + } + // Update the backend state s = sMgr.State() if s == nil { s = terraform.NewState() } s.Backend = &terraform.BackendState{ - Type: c.Type, - Config: c.RawConfig.Raw, - Hash: c.Hash, + Type: c.Type, + ConfigRaw: json.RawMessage(configJSON), + Hash: cHash, } if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(errBackendWriteSaved, err) + diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + return nil, diags } if err := sMgr.PersistState(); err != nil { - return nil, fmt.Errorf(errBackendWriteSaved, err) + diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + return nil, diags } if output { @@ -1076,245 +890,155 @@ func (m *Meta) backend_C_r_S_changed( "[reset][green]\n"+strings.TrimSpace(successBackendSet), s.Backend.Type))) } - return b, nil + return b, diags } // Initiailizing an unchanged saved backend -func (m *Meta) backend_C_r_S_unchanged( - c *config.Backend, sMgr state.State) (backend.Backend, error) { +func (m *Meta) backend_C_r_S_unchanged(c *configs.Backend, cHash int, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + s := sMgr.State() // it's possible for a backend to be unchanged, and the config itself to // have changed by moving a parameter from the config to `-backend-config` // In this case we only need to update the Hash. - if c != nil && s.Backend.Hash != c.Hash { - s.Backend.Hash = c.Hash + if c != nil && s.Backend.Hash != cHash { + s.Backend.Hash = cHash if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(errBackendWriteSaved, err) + diags = diags.Append(err) + return nil, diags } } - // Create the config. We do this from the backend state since this - // has the complete configuration data whereas the config itself - // may require input. - rawC, err := config.NewRawConfig(s.Backend.Config) - if err != nil { - return nil, fmt.Errorf("Error configuring backend: %s", err) - } - config := terraform.NewResourceConfig(rawC) - // Get the backend f := backendInit.Backend(s.Backend.Type) if f == nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) + return nil, diags } b := f() - // Configure - if err := b.Configure(config); err != nil { - return nil, fmt.Errorf(errBackendSavedConfig, s.Backend.Type, err) + // The configuration saved in the working directory state file is used + // in this case, since it will contain any additional values that + // were provided via -backend-config arguments on terraform init. + schema := b.ConfigSchema() + configVal, err := s.Backend.Config(schema) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to decode current backend config", + fmt.Sprintf("The backend configuration created by the most recent run of \"terraform init\" could not be decoded: %s. The configuration may have been initialized by an earlier version that used an incompatible configuration structure. Run \"terraform init -reconfigure\" to force re-initialization of the backend.", err), + )) + return nil, diags } - return b, nil + // Validate the config and then configure the backend + validDiags := b.ValidateConfig(configVal) + diags = diags.Append(validDiags) + if validDiags.HasErrors() { + return nil, diags + } + configDiags := b.Configure(configVal) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, diags + } + + return b, diags } // Initiailizing a changed saved backend with legacy remote state. -func (m *Meta) backend_C_R_S_changed( - c *config.Backend, sMgr state.State) (backend.Backend, error) { - // Notify the user - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset]%s\n\n", - strings.TrimSpace(outputBackendSavedWithLegacyChanged)))) +func (m *Meta) backend_C_R_S_changed(c *configs.Backend, sMgr state.State) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - // Reconfigure the backend first - if _, err := m.backend_C_r_S_changed(c, sMgr, false); err != nil { - return nil, err - } + m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") - // Handle the case where we have all set but unchanged - b, err := m.backend_C_R_S_unchanged(c, sMgr, false) - if err != nil { - return nil, err - } - - // Output success message - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset][green]\n\n"+ - strings.TrimSpace(successBackendReconfigureWithLegacy), c.Type))) - - return b, nil + diags = diags.Append(fmt.Errorf("Cannot initialize legacy remote state")) + return nil, diags } // Initiailizing an unchanged saved backend with legacy remote state. -func (m *Meta) backend_C_R_S_unchanged( - c *config.Backend, sMgr state.State, output bool) (backend.Backend, error) { - if output { - // Notify the user - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset]%s\n\n", - strings.TrimSpace(outputBackendSavedWithLegacy)))) - } +func (m *Meta) backend_C_R_S_unchanged(c *configs.Backend, sMgr state.State, output bool) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - // Load the backend from the state - s := sMgr.State() - b, err := m.backendInitFromSaved(s.Backend) - if err != nil { - return nil, err - } + m.Ui.Error(strings.TrimSpace(errBackendLegacy) + "\n") - m.Ui.Output(strings.TrimSpace(outputBackendMigrateLegacy)) - - // Initialize the legacy backend - oldB, err := m.backendInitFromLegacy(s.Remote) - if err != nil { - return nil, err - } - - // Perform the migration - err = m.backendMigrateState(&backendMigrateOpts{ - OneType: s.Remote.Type, - TwoType: s.Backend.Type, - One: oldB, - Two: b, - }) - if err != nil { - return nil, err - } - - if m.stateLock { - stateLocker := clistate.NewLocker(context.Background(), m.stateLockTimeout, m.Ui, m.Colorize()) - if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil { - return nil, fmt.Errorf("Error locking state: %s", err) - } - defer stateLocker.Unlock(nil) - } - - // Unset the remote state - s = sMgr.State() - if s == nil { - s = terraform.NewState() - } - s.Remote = nil - - if err := sMgr.WriteState(s); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err) - } - if err := sMgr.PersistState(); err != nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendClearLegacy), err) - } - - if output { - m.Ui.Output(m.Colorize().Color(fmt.Sprintf( - "[reset][green]\n\n"+ - strings.TrimSpace(successBackendLegacyUnset), s.Backend.Type))) - } - - return b, nil + diags = diags.Append(fmt.Errorf("Cannot initialize legacy remote state")) + return nil, diags } //------------------------------------------------------------------- // Reusable helper functions for backend management //------------------------------------------------------------------- -func (m *Meta) backendInitFromConfig(c *config.Backend) (backend.Backend, error) { - // Create the config. - config := terraform.NewResourceConfig(c.RawConfig) +func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics // Get the backend f := backendInit.Backend(c.Type) if f == nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendNewUnknown), c.Type) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendNewUnknown), c.Type)) + return nil, cty.NilVal, diags } b := f() + schema := b.ConfigSchema() + decSpec := schema.DecoderSpec() + configVal, hclDiags := hcldec.Decode(c.Config, decSpec, nil) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, cty.NilVal, diags + } + // TODO: test - // Ask for input if we have input enabled if m.Input() { var err error - config, err = b.Input(m.UIInput(), config) + configVal, err = m.inputForSchema(configVal, schema) if err != nil { - return nil, fmt.Errorf( - "Error asking for input to configure the backend %q: %s", - c.Type, err) + diags = diags.Append(fmt.Errorf("Error asking for input to configure backend %q: %s", c.Type, err)) } } - // Validate - warns, errs := b.Validate(config) - for _, warning := range warns { - // We just write warnings directly to the UI. This isn't great - // since we're a bit deep here to be pushing stuff out into the - // UI, but sufficient to let us print out deprecation warnings - // and the like. - m.Ui.Warn(warning) - } - if len(errs) > 0 { - return nil, fmt.Errorf( - "Error configuring the backend %q: %s", - c.Type, multierror.Append(nil, errs...)) + validateDiags := b.ValidateConfig(configVal) + diags = diags.Append(validateDiags.InConfigBody(c.Config)) + if validateDiags.HasErrors() { + return nil, cty.NilVal, diags } - // Configure - if err := b.Configure(config); err != nil { - return nil, fmt.Errorf(errBackendNewConfig, c.Type, err) - } + configureDiags := b.Configure(configVal) + diags = diags.Append(configureDiags.InConfigBody(c.Config)) - return b, nil + return b, configVal, diags } -func (m *Meta) backendInitFromLegacy(s *terraform.RemoteState) (backend.Backend, error) { - // We need to convert the config to map[string]interface{} since that - // is what the backends expect. - var configMap map[string]interface{} - if err := mapstructure.Decode(s.Config, &configMap); err != nil { - return nil, fmt.Errorf("Error configuring remote state: %s", err) - } - - // Create the config - rawC, err := config.NewRawConfig(configMap) - if err != nil { - return nil, fmt.Errorf("Error configuring remote state: %s", err) - } - config := terraform.NewResourceConfig(rawC) +func (m *Meta) backendInitFromSaved(s *terraform.BackendState) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics // Get the backend f := backendInit.Backend(s.Type) if f == nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendLegacyUnknown), s.Type) + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Type)) + return nil, diags } b := f() - // Configure - if err := b.Configure(config); err != nil { - return nil, fmt.Errorf(errBackendLegacyConfig, err) - } - - return b, nil -} - -func (m *Meta) backendInitFromSaved(s *terraform.BackendState) (backend.Backend, error) { - // Create the config. We do this from the backend state since this - // has the complete configuration data whereas the config itself - // may require input. - rawC, err := config.NewRawConfig(s.Config) + schema := b.ConfigSchema() + configVal, err := s.Config(schema) if err != nil { - return nil, fmt.Errorf("Error configuring backend: %s", err) - } - config := terraform.NewResourceConfig(rawC) - - // Get the backend - f := backendInit.Backend(s.Type) - if f == nil { - return nil, fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Type) - } - b := f() - - // Configure - if err := b.Configure(config); err != nil { - return nil, fmt.Errorf(errBackendSavedConfig, s.Type, err) + diags = diags.Append(errwrap.Wrapf("saved backend configuration is invalid: {{err}}", err)) + return nil, diags } - return b, nil + validateDiags := b.ValidateConfig(configVal) + diags = diags.Append(validateDiags) + if validateDiags.HasErrors() { + return nil, diags + } + + configureDiags := b.Configure(configVal) + diags = diags.Append(configureDiags) + + return b, diags } func (m *Meta) backendInitRequired(reason string) { @@ -1615,14 +1339,11 @@ Successfully configured the backend %q! Terraform will automatically use this backend unless the backend configuration changes. ` -const warnBackendLegacy = ` -Deprecation warning: This environment is configured to use legacy remote state. -Remote state changed significantly in Terraform 0.9. Please update your remote -state configuration to use the new 'backend' settings. For now, Terraform -will continue to use your existing settings. Legacy remote state support -will be removed in Terraform 0.11. +const errBackendLegacy = ` +This working directory is configured to use the legacy remote state features +from Terraform 0.8 or earlier. Remote state changed significantly in Terraform +0.9 and the automatic upgrade mechanism has now been removed. -You can find a guide for upgrading here: - -https://www.terraform.io/docs/backends/legacy-0-8.html +To upgrade, please first use Terraform v0.11 to complete the upgrade steps: + https://www.terraform.io/docs/backends/legacy-0-8.html ` diff --git a/command/meta_backend_test.go b/command/meta_backend_test.go index 871ba5ce5..e5ff66895 100644 --- a/command/meta_backend_test.go +++ b/command/meta_backend_test.go @@ -9,6 +9,9 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/configs" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/backend" backendInit "github.com/hashicorp/terraform/backend/init" backendLocal "github.com/hashicorp/terraform/backend/local" @@ -28,9 +31,9 @@ func TestMetaBackend_emptyDir(t *testing.T) { // Get the backend m := testMetaBackend(t, nil) - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Write some state @@ -98,9 +101,9 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) { // Get the backend m := testMetaBackend(t, nil) - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -171,9 +174,9 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { m.statePath = statePath // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -230,9 +233,9 @@ func TestMetaBackend_emptyLegacyRemote(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -297,9 +300,9 @@ func TestMetaBackend_configureNew(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -366,9 +369,9 @@ func TestMetaBackend_configureNewWithState(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -444,9 +447,9 @@ func TestMetaBackend_configureNewWithoutCopy(t *testing.T) { m.input = false // init the backend - _, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + _, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Verify the state is where we expect @@ -493,9 +496,9 @@ func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -537,9 +540,9 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { m.forceInitCopy = true // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -611,9 +614,9 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -685,9 +688,9 @@ func TestMetaBackend_configureNewLegacy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -779,9 +782,9 @@ func TestMetaBackend_configureNewLegacyCopy(t *testing.T) { m.forceInitCopy = true // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -872,9 +875,9 @@ func TestMetaBackend_configuredUnchanged(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -919,9 +922,9 @@ func TestMetaBackend_configuredChange(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1007,9 +1010,9 @@ func TestMetaBackend_reconfigureChange(t *testing.T) { m.reconfigure = true // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1051,9 +1054,9 @@ func TestMetaBackend_configuredChangeCopy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1105,9 +1108,9 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1160,9 +1163,9 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1215,9 +1218,9 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1286,9 +1289,9 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) } // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1342,9 +1345,9 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check resulting states @@ -1584,9 +1587,9 @@ func TestMetaBackend_configuredUnset(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1668,9 +1671,9 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1747,9 +1750,9 @@ func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1848,9 +1851,9 @@ func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) { m.forceInitCopy = true // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -1951,9 +1954,9 @@ func TestMetaBackend_configuredChangedLegacy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2051,9 +2054,9 @@ func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2154,9 +2157,9 @@ func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2257,9 +2260,9 @@ func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2360,9 +2363,9 @@ func TestMetaBackend_configuredUnsetWithLegacyNoCopy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2450,9 +2453,9 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2548,9 +2551,9 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2646,9 +2649,9 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Init: true}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2747,9 +2750,9 @@ func TestMetaBackend_planLocal(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Plan: plan}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Plan: plan}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2844,9 +2847,9 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { m.stateOutPath = statePath // Get the backend - b, err := m.Backend(&BackendOpts{Plan: plan}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Plan: plan}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -2930,9 +2933,9 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Plan: plan}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Plan: plan}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -3023,12 +3026,12 @@ func TestMetaBackend_planLocalMismatchLineage(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - _, err := m.Backend(&BackendOpts{Plan: plan}) - if err == nil { + _, diags := m.Backend(&BackendOpts{Plan: plan}) + if !diags.HasErrors() { t.Fatal("should have error") } - if !strings.Contains(err.Error(), "lineage") { - t.Fatalf("bad: %s", err) + if !strings.Contains(diags[0].Description().Summary, "lineage") { + t.Fatalf("wrong diagnostic message %q; want something containing \"lineage\"", diags[0].Description().Summary) } // Verify our local state didn't change @@ -3075,12 +3078,12 @@ func TestMetaBackend_planLocalNewer(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - _, err := m.Backend(&BackendOpts{Plan: plan}) - if err == nil { + _, diags := m.Backend(&BackendOpts{Plan: plan}) + if !diags.HasErrors() { t.Fatal("should have error") } - if !strings.Contains(err.Error(), "older") { - t.Fatalf("bad: %s", err) + if !strings.Contains(diags[0].Description().Summary, "older") { + t.Fatalf("wrong diagnostic message %q; want something containing \"older\"", diags[0].Description().Summary) } // Verify our local state didn't change @@ -3130,9 +3133,9 @@ func TestMetaBackend_planBackendEmptyDir(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Plan: plan}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Plan: plan}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -3232,9 +3235,9 @@ func TestMetaBackend_planBackendMatch(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Plan: plan}) - if err != nil { - t.Fatalf("bad: %s", err) + b, diags := m.Backend(&BackendOpts{Plan: plan}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -3337,12 +3340,12 @@ func TestMetaBackend_planBackendMismatchLineage(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - _, err := m.Backend(&BackendOpts{Plan: plan}) - if err == nil { + _, diags := m.Backend(&BackendOpts{Plan: plan}) + if !diags.HasErrors() { t.Fatal("should have error") } - if !strings.Contains(err.Error(), "lineage") { - t.Fatalf("bad: %s", err) + if !strings.Contains(diags[0].Description().Summary, "lineage") { + t.Fatalf("wrong diagnostic message %q; want something containing \"lineage\"", diags[0].Description().Summary) } // Verify our local state didn't change @@ -3395,9 +3398,9 @@ func TestMetaBackend_planLegacy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, err := m.Backend(&BackendOpts{Plan: plan}) - if err != nil { - t.Fatalf("err: %s", err) + b, diags := m.Backend(&BackendOpts{Plan: plan}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state @@ -3476,52 +3479,46 @@ func TestMetaBackend_configureWithExtra(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - extras := map[string]interface{}{"path": "hello"} + extras := map[string]cty.Value{"path": cty.StringVal("hello")} m := testMetaBackend(t, nil) opts := &BackendOpts{ - ConfigExtra: extras, - Init: true, + ConfigOverride: configs.SynthBody("synth", extras), + Init: true, } - backendCfg, err := m.backendConfig(opts) + _, cHash, err := m.backendConfig(opts) if err != nil { t.Fatal(err) } // init the backend - _, err = m.Backend(&BackendOpts{ - ConfigExtra: extras, - Init: true, + _, diags := m.Backend(&BackendOpts{ + ConfigOverride: configs.SynthBody("synth", extras), + Init: true, }) - if err != nil { - t.Fatalf("bad: %s", err) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // Check the state - s := testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) - if s.Backend.Hash != backendCfg.Hash { + s := testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) + if s.Backend.Hash != cHash { t.Fatal("mismatched state and config backend hashes") } - if s.Backend.Rehash() == s.Backend.Hash { - t.Fatal("saved hash should not match actual hash") - } - if s.Backend.Rehash() != backendCfg.Rehash() { - t.Fatal("mismatched state and config re-hashes") - } // init the backend again with the same options m = testMetaBackend(t, nil) _, err = m.Backend(&BackendOpts{ - ConfigExtra: extras, - Init: true, + ConfigOverride: configs.SynthBody("synth", extras), + Init: true, }) if err != nil { t.Fatalf("bad: %s", err) } // Check the state - s = testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) - if s.Backend.Hash != backendCfg.Hash { + s = testStateRead(t, filepath.Join(DefaultDataDir, backendlocal.DefaultStateFilename)) + if s.Backend.Hash != cHash { t.Fatal("mismatched state and config backend hashes") } } @@ -3557,11 +3554,9 @@ func TestMetaBackend_localDoesNotDeleteLocal(t *testing.T) { m := testMetaBackend(t, nil) m.forceInitCopy = true // init the backend - _, err = m.Backend(&BackendOpts{ - Init: true, - }) - if err != nil { - t.Fatalf("bad: %s", err) + _, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) } // check that we can read the state @@ -3599,15 +3594,15 @@ func TestMetaBackend_configToExtra(t *testing.T) { } // init the backend again with the options - extras := map[string]interface{}{"path": "hello"} + extras := map[string]cty.Value{"path": cty.StringVal("hello")} m = testMetaBackend(t, nil) m.forceInitCopy = true - _, err = m.Backend(&BackendOpts{ - ConfigExtra: extras, - Init: true, + _, diags := m.Backend(&BackendOpts{ + ConfigOverride: configs.SynthBody("synth", extras), + Init: true, }) - if err != nil { - t.Fatalf("bad: %s", err) + if diags.HasErrors() { + t.Fatal(diags.Err()) } s = testStateRead(t, filepath.Join(DefaultDataDir, backendLocal.DefaultStateFilename)) diff --git a/command/meta_config.go b/command/meta_config.go index 66e4fe683..3e2e629d0 100644 --- a/command/meta_config.go +++ b/command/meta_config.go @@ -4,11 +4,18 @@ import ( "fmt" "os" "path/filepath" + "sort" + "github.com/hashicorp/hcl2/hcl/hclsyntax" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/terraform/config/configschema" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs/configload" + "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" ) // normalizePath normalizes a given path so that it is, if possible, relative @@ -80,6 +87,23 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic return module, diags } +// dirIsConfigPath checks if the given path is a directory that contains at +// least one Terraform configuration file (.tf or .tf.json), returning true +// if so. +// +// In the unlikely event that the underlying config loader cannot be initalized, +// this function optimistically returns true, assuming that the caller will +// then do some other operation that requires the config loader and get an +// error at that point. +func (m *Meta) dirIsConfigPath(dir string) bool { + loader, err := m.initConfigLoader() + if err != nil { + return true + } + + return loader.IsConfigDir(dir) +} + // 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. @@ -99,6 +123,41 @@ func (m *Meta) loadBackendConfig(rootDir string) (*configs.Backend, tfdiags.Diag return mod.Backend, diags } +// loadValuesFile loads a file that defines a single map of key/value pairs. +// This is the format used for "tfvars" files. +func (m *Meta) loadValuesFile(filename string) (map[string]cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + filename = m.normalizePath(filename) + + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + vals, hclDiags := loader.Parser().LoadValuesFile(filename) + diags = diags.Append(hclDiags) + return vals, diags +} + +// loadHCLFile reads an arbitrary HCL file and returns the unprocessed body +// representing its toplevel. Most callers should use one of the more +// specialized "load..." methods to get a higher-level representation. +func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + filename = m.normalizePath(filename) + + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + body, hclDiags := loader.Parser().LoadHCLFile(filename) + diags = diags.Append(hclDiags) + return body, diags +} + // installModules reads a root module from the given directory and attempts // recursively install all of its descendent modules. // @@ -172,6 +231,67 @@ func (m *Meta) loadVarsFile(filename string) (map[string]cty.Value, tfdiags.Diag return ret, diags } +// inputForSchema uses interactive prompts to try to populate any +// not-yet-populated required attributes in the given object value to +// comply with the given schema. +// +// An error will be returned if input is disabled for this meta or if +// values cannot be obtained for some other operational reason. Errors are +// not returned for invalid input since the input loop itself will report +// that interactively. +// +// It is not guaranteed that the result will be valid, since certain attribute +// types and nested blocks are not supported for input. +// +// The given value must conform to the given schema. If not, this method will +// panic. +func (m *Meta) inputForSchema(given cty.Value, schema *configschema.Block) (cty.Value, error) { + if given.IsNull() || !given.IsKnown() { + // This is not reasonable input, but we'll tolerate it anyway and + // just pass it through for the caller to handle downstream. + return given, nil + } + + givenVals := given.AsValueMap() + retVals := make(map[string]cty.Value, len(givenVals)) + names := make([]string, 0, len(schema.Attributes)) + for name, attrS := range schema.Attributes { + retVals[name] = givenVals[name] + if givenVal := givenVals[name]; attrS.Required && givenVal.IsNull() && attrS.Type.IsPrimitiveType() { + names = append(names, name) + } + } + sort.Strings(names) + + input := m.UIInput() + for _, name := range names { + attrS := schema.Attributes[name] + + for { + strVal, err := input.Input(&terraform.InputOpts{ + Id: name, + Query: name, + Description: attrS.Description, + }) + if err != nil { + return cty.UnknownVal(schema.ImpliedType()), fmt.Errorf("%s: %s", name, err) + } + + val := cty.StringVal(strVal) + val, err = convert.Convert(val, attrS.Type) + if err != nil { + m.showDiagnostics(fmt.Errorf("Invalid value: %s", err)) + continue + } + + retVals[name] = val + break + } + } + + return cty.ObjectVal(retVals), nil +} + // configSources returns the source cache from the receiver's config loader, // which the caller must not modify. // @@ -211,3 +331,87 @@ func (m *Meta) initConfigLoader() (*configload.Loader, error) { } return m.configLoader, nil } + +// configValueFromCLI parses a configuration value that was provided in a +// context in the CLI where only strings can be provided, such as on the +// command line or in an environment variable, and returns the resulting +// value. +func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + switch { + case wantType.IsPrimitiveType(): + // Primitive types are handled as conversions from string. + val := cty.StringVal(rawValue) + var err error + val, err = convert.Convert(val, wantType) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid backend configuration value", + fmt.Sprintf("Invalid backend configuration argument %s: %s", synthFilename, err), + )) + val = cty.DynamicVal // just so we return something valid-ish + } + return val, diags + default: + // Non-primitives are parsed as HCL expressions + src := []byte(rawValue) + expr, hclDiags := hclsyntax.ParseExpression(src, synthFilename, hcl.Pos{Line: 1, Column: 1}) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return cty.DynamicVal, diags + } + val, hclDiags := expr.Value(nil) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + val = cty.DynamicVal + } + return val, diags + } +} + +// rawFlags is a flag.Value implementation that just appends raw flag +// names and values to a slice. +type rawFlags struct { + flagName string + items *[]rawFlag +} + +func newRawFlags(flagName string) rawFlags { + return rawFlags{ + flagName: flagName, + } +} + +func (f rawFlags) AllItems() []rawFlag { + return *f.items +} + +func (f rawFlags) Alias(flagName string) rawFlags { + return rawFlags{ + flagName: flagName, + items: f.items, + } +} + +func (f rawFlags) String() string { + return "" +} + +func (f rawFlags) Set(str string) error { + *f.items = append(*f.items, rawFlag{ + Name: f.flagName, + Value: str, + }) + return nil +} + +type rawFlag struct { + Name string + Value string +} + +func (f rawFlag) String() string { + return fmt.Sprintf("%s=%q", f.Name, f.Value) +} diff --git a/command/output.go b/command/output.go index 4b8d57d07..7ab2af9f6 100644 --- a/command/output.go +++ b/command/output.go @@ -7,6 +7,8 @@ import ( "fmt" "sort" "strings" + + "github.com/hashicorp/terraform/tfdiags" ) // OutputCommand is a Command implementation that reads an output @@ -46,10 +48,13 @@ func (c *OutputCommand) Run(args []string) int { name = args[0] } + var diags tfdiags.Diagnostics + // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/plan.go b/command/plan.go index 06b8d2ca8..fe262b0a9 100644 --- a/command/plan.go +++ b/command/plan.go @@ -5,8 +5,7 @@ import ( "strings" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/tfdiags" ) @@ -70,58 +69,62 @@ func (c *PlanCommand) Run(args []string) int { var diags tfdiags.Diagnostics - // Load the module if we don't have one yet (not running from plan) - var mod *module.Tree + var backendConfig *configs.Backend if plan == nil { - var modDiags tfdiags.Diagnostics - mod, modDiags = c.Module(configPath) - diags = diags.Append(modDiags) - if modDiags.HasErrors() { + var configDiags tfdiags.Diagnostics + backendConfig, configDiags = c.loadBackendConfig(configPath) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { c.showDiagnostics(diags) return 1 } } - var conf *config.Config - if mod != nil { - conf = mod.Config() - } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, Plan: plan, }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } + // Emit any diagnostics we've accumulated before we delegate to the + // backend, since the backend will handle its own diagnostics internally. + c.showDiagnostics(diags) + diags = nil + // Build the operation opReq := c.Operation() opReq.Destroy = destroy - opReq.Module = mod - opReq.ModuleDepth = moduleDepth + opReq.ConfigDir = configPath opReq.Plan = plan opReq.PlanOutPath = outPath opReq.PlanRefresh = refresh opReq.Type = backend.OperationTypePlan + opReq.ConfigLoader, err = c.initConfigLoader() + if err != nil { + c.showDiagnostics(err) + return 1 + } // Perform the operation op, err := c.RunOperation(b, opReq) if err != nil { - diags = diags.Append(err) - } - - c.showDiagnostics(diags) - if diags.HasErrors() { + c.showDiagnostics(err) return 1 } + if op.Result != backend.OperationSuccess { + return op.Result.ExitStatus() + } if detailed && !op.PlanEmpty { return 2 } - return op.ExitCode + return op.Result.ExitStatus() } func (c *PlanCommand) Help() string { diff --git a/command/providers.go b/command/providers.go index 49d43962e..83341bccd 100644 --- a/command/providers.go +++ b/command/providers.go @@ -5,7 +5,7 @@ import ( "sort" "github.com/hashicorp/terraform/moduledeps" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" "github.com/xlab/treeprint" ) @@ -38,26 +38,22 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } - // Load the config - root, diags := c.Module(configPath) - if diags.HasErrors() { + var diags tfdiags.Diagnostics + + config, configDiags := c.loadConfig(configPath) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { c.showDiagnostics(diags) return 1 } - if root == nil { - c.Ui.Error(fmt.Sprintf( - "No configuration files found in the directory: %s\n\n"+ - "This command requires configuration to run.", - configPath)) - return 1 - } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: root.Config(), + b, backendDiags := c.Backend(&BackendOpts{ + Config: config.Module.Backend, }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } @@ -73,9 +69,11 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } - s := state.State() - - depTree := terraform.ModuleTreeDependencies(root, s) + // FIXME: Restore this once the "terraform" package is updated to deal + // with HCL2 config types. + //s := state.State() + //depTree := terraform.ModuleTreeDependencies(config, s) + var depTree *moduledeps.Module depTree.SortDescendents() printRoot := treeprint.New() diff --git a/command/refresh.go b/command/refresh.go index ce61c4233..42579130f 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -5,8 +5,6 @@ import ( "strings" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) @@ -42,54 +40,62 @@ func (c *RefreshCommand) Run(args []string) int { var diags tfdiags.Diagnostics - // Load the module - mod, diags := c.Module(configPath) - if diags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - // Check for user-supplied plugin path if c.pluginPath, err = c.loadPluginPath(); err != nil { c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) return 1 } - var conf *config.Config - if mod != nil { - conf = mod.Config() + backendConfig, configDiags := c.loadBackendConfig(configPath) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } + // Before we delegate to the backend, we'll print any warning diagnostics + // we've accumulated here, since the backend will start fresh with its own + // diagnostics. + c.showDiagnostics(diags) + diags = nil + // Build the operation opReq := c.Operation() opReq.Type = backend.OperationTypeRefresh - opReq.Module = mod - - op, err := c.RunOperation(b, opReq) + opReq.ConfigDir = configPath + opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { - diags = diags.Append(err) - } - - c.showDiagnostics(diags) - if diags.HasErrors() { + c.showDiagnostics(err) return 1 } - // Output the outputs - if outputs := outputsAsString(op.State, terraform.RootModulePath, nil, true); outputs != "" { - c.Ui.Output(c.Colorize().Color(outputs)) + op, err := c.RunOperation(b, opReq) + if err != nil { + c.showDiagnostics(err) + return 1 + } + if op.Result != backend.OperationSuccess { + return op.Result.ExitStatus() } - return 0 + // TODO: Print outputs, once this is updated to use new config types. + /* + if outputs := outputsAsString(op.State, terraform.RootModulePath, nil, true); outputs != "" { + c.Ui.Output(c.Colorize().Color(outputs)) + } + */ + + return op.Result.ExitStatus() } func (c *RefreshCommand) Help() string { diff --git a/command/show.go b/command/show.go index d8d1c84ef..bc24cc993 100644 --- a/command/show.go +++ b/command/show.go @@ -71,9 +71,9 @@ func (c *ShowCommand) Run(args []string) int { } } else { // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } diff --git a/command/state_list.go b/command/state_list.go index ba0bc9e38..4913c67b4 100644 --- a/command/state_list.go +++ b/command/state_list.go @@ -30,9 +30,9 @@ func (c *StateListCommand) Run(args []string) int { args = cmdFlags.Args() // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } diff --git a/command/state_meta.go b/command/state_meta.go index a1648358b..334e03a28 100644 --- a/command/state_meta.go +++ b/command/state_meta.go @@ -31,9 +31,9 @@ func (c *StateMeta) State() (state.State, error) { } } else { // Load the backend - b, err := c.Backend(nil) - if err != nil { - return nil, err + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + return nil, backendDiags.Err() } env := c.Workspace() @@ -44,10 +44,10 @@ func (c *StateMeta) State() (state.State, error) { } // Get a local backend - localRaw, err := c.Backend(&BackendOpts{ForceLocal: true}) - if err != nil { + localRaw, backendDiags := c.Backend(&BackendOpts{ForceLocal: true}) + if backendDiags.HasErrors() { // This should never fail - panic(err) + panic(backendDiags.Err()) } localB := localRaw.(*backendLocal.Local) _, stateOutPath, _ = localB.StatePaths(env) diff --git a/command/state_pull.go b/command/state_pull.go index c010d2a5e..6b211ca04 100644 --- a/command/state_pull.go +++ b/command/state_pull.go @@ -28,9 +28,9 @@ func (c *StatePullCommand) Run(args []string) int { args = cmdFlags.Args() // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } diff --git a/command/state_push.go b/command/state_push.go index 5b7d76978..5455fcc51 100644 --- a/command/state_push.go +++ b/command/state_push.go @@ -63,9 +63,9 @@ func (c *StatePushCommand) Run(args []string) int { } // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } diff --git a/command/state_show.go b/command/state_show.go index d44d0c07e..5d7443965 100644 --- a/command/state_show.go +++ b/command/state_show.go @@ -30,9 +30,9 @@ func (c *StateShowCommand) Run(args []string) int { args = cmdFlags.Args() // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } diff --git a/command/taint.go b/command/taint.go index 4e0ea6a69..44b4d87c1 100644 --- a/command/taint.go +++ b/command/taint.go @@ -64,9 +64,9 @@ func (c *TaintCommand) Run(args []string) int { } // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } diff --git a/command/unlock.go b/command/unlock.go index 4ac504972..21f8630ec 100644 --- a/command/unlock.go +++ b/command/unlock.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" ) @@ -46,18 +47,22 @@ func (c *UnlockCommand) Run(args []string) int { return 1 } - conf, err := c.Config(configPath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + var diags tfdiags.Diagnostics + + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/untaint.go b/command/untaint.go index cc6923f08..47e86c69e 100644 --- a/command/untaint.go +++ b/command/untaint.go @@ -52,9 +52,9 @@ func (c *UntaintCommand) Run(args []string) int { } // Load the backend - b, err := c.Backend(nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) return 1 } diff --git a/command/workspace_delete.go b/command/workspace_delete.go index 1e1eb1182..afc78384b 100644 --- a/command/workspace_delete.go +++ b/command/workspace_delete.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -49,19 +50,22 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { return 1 } - cfg, err := c.Config(configPath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + var diags tfdiags.Diagnostics + + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: cfg, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) - - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/workspace_list.go b/command/workspace_list.go index da7f9b19c..5ddd02090 100644 --- a/command/workspace_list.go +++ b/command/workspace_list.go @@ -2,9 +2,9 @@ package command import ( "bytes" - "fmt" "strings" + "github.com/hashicorp/terraform/tfdiags" "github.com/posener/complete" ) @@ -34,19 +34,22 @@ func (c *WorkspaceListCommand) Run(args []string) int { return 1 } - cfg, err := c.Config(configPath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + var diags tfdiags.Diagnostics + + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: cfg, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) - - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/workspace_new.go b/command/workspace_new.go index 810a776fc..d5232342c 100644 --- a/command/workspace_new.go +++ b/command/workspace_new.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform/command/clistate" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -59,18 +60,22 @@ func (c *WorkspaceNewCommand) Run(args []string) int { return 1 } - conf, err := c.Config(configPath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + var diags tfdiags.Diagnostics + + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) - - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) return 1 } diff --git a/command/workspace_select.go b/command/workspace_select.go index 7070cc611..1335d328c 100644 --- a/command/workspace_select.go +++ b/command/workspace_select.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform/tfdiags" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -38,9 +39,13 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { return 1 } - conf, err := c.Config(configPath) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err)) + var diags tfdiags.Diagnostics + + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 } current, isOverridden := c.WorkspaceOverridden() @@ -50,9 +55,14 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { } // Load the backend - b, err := c.Backend(&BackendOpts{ - Config: conf, + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, }) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } if err != nil { c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err)) diff --git a/config/configschema/none_required.go b/config/configschema/none_required.go new file mode 100644 index 000000000..0be3b8fa3 --- /dev/null +++ b/config/configschema/none_required.go @@ -0,0 +1,38 @@ +package configschema + +// NoneRequired returns a deep copy of the receiver with any required +// attributes translated to optional. +func (b *Block) NoneRequired() *Block { + ret := &Block{} + + if b.Attributes != nil { + ret.Attributes = make(map[string]*Attribute, len(b.Attributes)) + } + for name, attrS := range b.Attributes { + ret.Attributes[name] = attrS.forceOptional() + } + + if b.BlockTypes != nil { + ret.BlockTypes = make(map[string]*NestedBlock, len(b.BlockTypes)) + } + for name, blockS := range b.BlockTypes { + ret.BlockTypes[name] = blockS.noneRequired() + } + + return ret +} + +func (b *NestedBlock) noneRequired() *NestedBlock { + ret := *b + ret.Block = *(ret.Block.NoneRequired()) + ret.MinItems = 0 + ret.MaxItems = 0 + return &ret +} + +func (a *Attribute) forceOptional() *Attribute { + ret := *a + ret.Optional = true + ret.Required = false + return &ret +} diff --git a/configs/backend.go b/configs/backend.go index 20dc97657..56cd04f41 100644 --- a/configs/backend.go +++ b/configs/backend.go @@ -2,6 +2,9 @@ package configs import ( "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcldec" + "github.com/hashicorp/terraform/config/configschema" + "github.com/zclconf/go-cty/cty" ) // Backend represents a "backend" block inside a "terraform" block in a module @@ -22,3 +25,31 @@ func decodeBackendBlock(block *hcl.Block) (*Backend, hcl.Diagnostics) { DeclRange: block.DefRange, }, nil } + +// Hash produces a hash value for the reciever that covers the type and the +// portions of the config that conform to the given schema. +// +// If the config does not conform to the schema then the result is not +// meaningful for comparison since it will be based on an incomplete result. +// +// As an exception, required attributes in the schema are treated as optional +// for the purpose of hashing, so that an incomplete configuration can still +// be hashed. Other errors, such as extraneous attributes, have no such special +// case. +func (b *Backend) Hash(schema *configschema.Block) int { + // Don't fail if required attributes are not set. Instead, we'll just + // hash them as nulls. + schema = schema.NoneRequired() + spec := schema.DecoderSpec() + val, _ := hcldec.Decode(b.Config, spec, nil) + if val == cty.NilVal { + val = cty.UnknownVal(schema.ImpliedType()) + } + + toHash := cty.TupleVal([]cty.Value{ + cty.StringVal(b.Type), + val, + }) + + return toHash.Hash() +} diff --git a/configs/config.go b/configs/config.go index e9b23b7a7..308d2e330 100644 --- a/configs/config.go +++ b/configs/config.go @@ -113,3 +113,21 @@ func (c *Config) AllModules() []*Config { }) return ret } + +// Descendent returns the descendent config that has the given path beneath +// the receiver, or nil if there is no such module. +// +// The path traverses the static module tree, prior to any expansion to handle +// count and for_each arguments. +// +// An empty path will just return the receiver, and is therefore pointless. +func (c *Config) Descendent(path []string) *Config { + current := c + for _, name := range path { + current = current.Children[name] + if current == nil { + return nil + } + } + return current +} diff --git a/terraform/resource_address.go b/terraform/resource_address.go index a64f5d846..ee7f2117b 100644 --- a/terraform/resource_address.go +++ b/terraform/resource_address.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/configs" ) // ResourceAddress is a way of identifying an individual resource (or, @@ -109,30 +109,35 @@ func (r *ResourceAddress) WholeModuleAddress() *ResourceAddress { } } -// MatchesConfig returns true if the receiver matches the given -// configuration resource within the given configuration module. +// MatchesManagedResourceConfig returns true if the receiver matches the given +// configuration resource within the given _static_ module path. Note that +// the module path in a resource address is a _dynamic_ module path, and +// multiple dynamic resource paths may map to a single static path if +// count and for_each are in use on module calls. // // Since resource configuration blocks represent all of the instances of // a multi-instance resource, the index of the address (if any) is not // considered. -func (r *ResourceAddress) MatchesConfig(mod *module.Tree, rc *config.Resource) bool { +func (r *ResourceAddress) MatchesManagedResourceConfig(path []string, rc *configs.ManagedResource) bool { if r.HasResourceSpec() { - if r.Mode != rc.Mode || r.Type != rc.Type || r.Name != rc.Name { + if r.Mode != config.ManagedResourceMode { + return false + } + if r.Type != rc.Type || r.Name != rc.Name { return false } } addrPath := r.Path - cfgPath := mod.Path() // normalize if len(addrPath) == 0 { addrPath = nil } - if len(cfgPath) == 0 { - cfgPath = nil + if len(path) == 0 { + path = nil } - return reflect.DeepEqual(addrPath, cfgPath) + return reflect.DeepEqual(addrPath, path) } // stateId returns the ID that this resource should be entered with diff --git a/terraform/resource_address_test.go b/terraform/resource_address_test.go index 0fe561528..c286984cc 100644 --- a/terraform/resource_address_test.go +++ b/terraform/resource_address_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/configs" ) func TestParseResourceAddressInternal(t *testing.T) { @@ -1046,16 +1046,17 @@ func TestResourceAddressWholeModuleAddress(t *testing.T) { } } -func TestResourceAddressMatchesConfig(t *testing.T) { - root := testModule(t, "empty-with-child-module") - child := root.Child([]string{"child"}) - grandchild := root.Child([]string{"child", "grandchild"}) +func TestResourceAddressMatchesManagedResourceConfig(t *testing.T) { + root := []string(nil) + child := []string{"child"} + grandchild := []string{"child", "grandchild"} + irrelevant := []string{"irrelevant"} tests := []struct { - Addr *ResourceAddress - Module *module.Tree - Resource *config.Resource - Want bool + Addr *ResourceAddress + ModulePath []string + Resource *configs.ManagedResource + Want bool }{ { &ResourceAddress{ @@ -1065,8 +1066,7 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Index: -1, }, root, - &config.Resource{ - Mode: config.ManagedResourceMode, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1081,8 +1081,7 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Index: -1, }, child, - &config.Resource{ - Mode: config.ManagedResourceMode, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1097,8 +1096,7 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Index: -1, }, grandchild, - &config.Resource{ - Mode: config.ManagedResourceMode, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1110,8 +1108,7 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Index: -1, }, child, - &config.Resource{ - Mode: config.ManagedResourceMode, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1123,8 +1120,7 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Index: -1, }, grandchild, - &config.Resource{ - Mode: config.ManagedResourceMode, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1137,9 +1133,8 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Name: "baz", Index: -1, }, - module.NewEmptyTree(), - &config.Resource{ - Mode: config.ManagedResourceMode, + irrelevant, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1152,9 +1147,8 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Name: "baz", Index: -1, }, - module.NewEmptyTree(), - &config.Resource{ - Mode: config.ManagedResourceMode, + irrelevant, + &configs.ManagedResource{ Type: "null_resource", Name: "pizza", }, @@ -1167,9 +1161,8 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Name: "baz", Index: -1, }, - module.NewEmptyTree(), - &config.Resource{ - Mode: config.ManagedResourceMode, + irrelevant, + &configs.ManagedResource{ Type: "aws_instance", Name: "baz", }, @@ -1184,8 +1177,7 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Index: -1, }, child, - &config.Resource{ - Mode: config.ManagedResourceMode, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1200,8 +1192,7 @@ func TestResourceAddressMatchesConfig(t *testing.T) { Index: -1, }, grandchild, - &config.Resource{ - Mode: config.ManagedResourceMode, + &configs.ManagedResource{ Type: "null_resource", Name: "baz", }, @@ -1211,11 +1202,11 @@ func TestResourceAddressMatchesConfig(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprintf("%02d-%s", i, test.Addr), func(t *testing.T) { - got := test.Addr.MatchesConfig(test.Module, test.Resource) + got := test.Addr.MatchesManagedResourceConfig(test.ModulePath, test.Resource) if got != test.Want { t.Errorf( "wrong result\naddr: %s\nmod: %#v\nrsrc: %#v\ngot: %#v\nwant: %#v", - test.Addr, test.Module.Path(), test.Resource, got, test.Want, + test.Addr, test.ModulePath, test.Resource, got, test.Want, ) } }) diff --git a/terraform/state.go b/terraform/state.go index 04b14a659..f986971c9 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -20,9 +20,12 @@ import ( "github.com/hashicorp/go-uuid" "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/config" - "github.com/mitchellh/copystructure" - + "github.com/hashicorp/terraform/config/configschema" tfversion "github.com/hashicorp/terraform/version" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/mitchellh/copystructure" ) const ( @@ -811,13 +814,9 @@ func (s *State) String() string { // BackendState stores the configuration to connect to a remote backend. type BackendState struct { - Type string `json:"type"` // Backend type - Config map[string]interface{} `json:"config"` // Backend raw config - - // Hash is the hash code to uniquely identify the original source - // configuration. We use this to detect when there is a change in - // configuration even when "type" isn't changed. - Hash uint64 `json:"hash"` + Type string `json:"type"` // Backend type + ConfigRaw json.RawMessage `json:"config"` // Backend raw config + Hash int `json:"hash"` // Hash of portion of configuration from config files } // Empty returns true if BackendState has no state. @@ -825,25 +824,29 @@ func (s *BackendState) Empty() bool { return s == nil || s.Type == "" } -// Rehash returns a unique content hash for this backend's configuration -// as a uint64 value. -// The Hash stored in the backend state needs to match the config itself, but -// we need to compare the backend config after it has been combined with all -// options. -// This function must match the implementation used by config.Backend. -func (s *BackendState) Rehash() uint64 { - if s == nil { - return 0 - } +// Config decodes the type-specific configuration object using the provided +// schema and returns the result as a cty.Value. +// +// An error is returned if the stored configuration does not conform to the +// given schema. +func (s *BackendState) Config(schema *configschema.Block) (cty.Value, error) { + ty := schema.ImpliedType() + return ctyjson.Unmarshal(s.ConfigRaw, ty) +} - cfg := config.Backend{ - Type: s.Type, - RawConfig: &config.RawConfig{ - Raw: s.Config, - }, +// SetConfig replaces (in-place) the type-specific configuration object using +// the provided value and associated schema. +// +// An error is returned if the given value does not conform to the implied +// type of the schema. +func (s *BackendState) SetConfig(val cty.Value, schema *configschema.Block) error { + ty := schema.ImpliedType() + buf, err := ctyjson.Marshal(val, ty) + if err != nil { + return err } - - return cfg.Rehash() + s.ConfigRaw = buf + return nil } // RemoteState is used to track the information about a remote