diff --git a/command/meta_backend.go b/command/meta_backend.go index 7596b45d5..3c38a8ea5 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -64,6 +64,10 @@ 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. +// +// A side-effect of this method is the population of m.backendState, recording +// the final resolved backend configuration after dealing with overrides from +// the "terraform init" command line, etc. func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -124,6 +128,29 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics panic(err) } + // If we got here from backendFromConfig returning nil then m.backendState + // won't be set, since that codepath considers that to be no backend at all, + // but our caller considers that to be the local backend with no config + // and so we'll synthesize a backend state so other code doesn't need to + // care about this special case. + // + // FIXME: We should refactor this so that we more directly and explicitly + // treat the local backend as the default, including in the UI shown to + // the user, since the local backend should only be used when learning or + // in exceptional cases and so it's better to help the user learn that + // by introducing it as a concept. + if m.backendState == nil { + // NOTE: This synthetic object is intentionally _not_ retained in the + // on-disk record of the backend configuration, which was already dealt + // with inside backendFromConfig, because we still need that codepath + // to be able to recognize the lack of a config as distinct from + // explicitly setting local until we do some more refactoring here. + m.backendState = &terraform.BackendState{ + Type: "local", + ConfigRaw: json.RawMessage("{}"), + } + } + return local, nil } @@ -323,6 +350,23 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return nil, diags } + // ------------------------------------------------------------------------ + // For historical reasons, current backend configuration for a working + // directory is kept in a *state-like* file, using the legacy state + // structures in the Terraform package. It is not actually a Terraform + // state, and so only the "backend" portion of it is actually used. + // + // The remainder of this code often confusingly refers to this as a "state", + // so it's unfortunately important to remember that this is not actually + // what we _usually_ think of as "state", and is instead a local working + // directory "backend configuration state" that is never persisted anywhere. + // + // Since the "real" state has since moved on to be represented by + // states.State, we can recognize the special meaning of state that applies + // to this function and its callees by their continued use of the + // otherwise-obsolete terraform.State. + // ------------------------------------------------------------------------ + // Get the path to where we store a local cache of backend configuration // if we're using a remote backend. This may not yet exist which means // we haven't used a non-local backend before. That is okay. diff --git a/command/plan.go b/command/plan.go index 790162123..1d994ce47 100644 --- a/command/plan.go +++ b/command/plan.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/tfdiags" ) @@ -108,6 +109,61 @@ func (c *PlanCommand) Run(args []string) int { return 1 } + // c.Backend above has a non-obvious side-effect of also populating + // c.backendState, which is the state-shaped formulation of the effective + // backend configuration after evaluation of the backend configuration. + // We will in turn adapt that to a plans.Backend to include in a plan file + // if opReq.PlanOutPath was set to a non-empty value above. + // + // FIXME: It's ugly to be doing this inline here, but it's also not really + // clear where would be better to do it. In future we should find a better + // home for this logic, and ideally also stop depending on the side-effect + // of c.Backend setting c.backendState. + { + // This is not actually a state in the usual sense, but rather a + // representation of part of the current working directory's + // "configuration state". + backendPseudoState := c.backendState + if backendPseudoState == nil { + // Should never happen if c.Backend is behaving properly. + diags = diags.Append(fmt.Errorf("Backend initialization didn't produce resolved configuration (This is a bug in Terraform)")) + c.showDiagnostics(diags) + return 1 + } + var backendForPlan plans.Backend + backendForPlan.Type = backendPseudoState.Type + backendForPlan.Workspace = c.Workspace() + + // Configuration is a little more awkward to handle here because it's + // stored in state as raw JSON but we need it as a plans.DynamicValue + // to save it in the state. To do that conversion we need to know the + // configuration schema of the backend. + configSchema := b.ConfigSchema() + config, err := backendPseudoState.Config(configSchema) + if err != nil { + // This means that the stored settings don't conform to the current + // schema, which could either be because we're reading something + // created by an older version that is no longer compatible, or + // because the user manually tampered with the stored config. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid backend initialization", + fmt.Sprintf("The backend configuration for this working directory is not valid: %s.\n\nIf you have recently upgraded Terraform, you may need to re-run \"terraform init\" to re-initialize this working directory."), + )) + c.showDiagnostics(diags) + return 1 + } + configForPlan, err := plans.NewDynamicValue(config, configSchema.ImpliedType()) + if err != nil { + // This should never happen, since we've just decoded this value + // using the same schema. + diags = diags.Append(fmt.Errorf("Failed to encode backend configuration to store in plan: %s", err)) + c.showDiagnostics(diags) + return 1 + } + backendForPlan.Config = configForPlan + } + // Perform the operation op, err := c.RunOperation(b, opReq) if err != nil {