From 7b28a5d842a098e731681ccdc0121297743ae732 Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:03:57 +0100 Subject: [PATCH] PSS: Allow backend or state_store config to be passed via BackendOpts into method for initialising the operations backend for a command (#37346) * Allow backend or state_store config to be passed via BackendOpts from calling code * Update messages sent to view: make message specific to state storage mechanism in use * Add nil pointer check * Fix typos * Pivot to `Len` method approach of nil check * Pivot to the point of pirouetting --- internal/command/init.go | 158 +++++++++++++++++++++++++++++---- internal/command/views/init.go | 5 ++ 2 files changed, 145 insertions(+), 18 deletions(-) diff --git a/internal/command/init.go b/internal/command/init.go index 805cf807fd..8f776597fe 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -8,7 +8,9 @@ import ( "errors" "fmt" "log" + "maps" "reflect" + "slices" "sort" "strings" @@ -28,6 +30,7 @@ import ( "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/didyoumean" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/providercache" "github.com/hashicorp/terraform/internal/states" @@ -390,7 +393,7 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize HCP Terraform") - _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here + _ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here defer span.End() view.Output(views.InitializingTerraformCloudMessage) @@ -419,14 +422,125 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize backend") - _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here + _ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here defer span.End() - view.Output(views.InitializingBackendMessage) + if root.StateStore != nil { + view.Output(views.InitializingStateStoreMessage) + } else { + view.Output(views.InitializingBackendMessage) + } + + var opts *BackendOpts + switch { + case root.StateStore != nil && root.Backend != nil: + // We expect validation during config parsing to prevent mutually exclusive backend and state_store blocks, + // but checking here just in case. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Conflicting backend and state_store configurations present during init", + Detail: fmt.Sprintf("When initializing the backend there was configuration data present for both backend %q and state store %q. This is a bug in Terraform and should be reported.", + root.Backend.Type, + root.StateStore.Type, + ), + Subject: &root.Backend.TypeRange, + }) + return nil, true, diags + case root.StateStore != nil: + // state_store config present + // Access provider factories + ctxOpts, err := c.contextOpts() + if err != nil { + diags = diags.Append(err) + return nil, true, diags + } + + if root.StateStore.ProviderAddr.IsZero() { + // This should not happen; this data is populated when parsing config, + // even for builtin providers + panic(fmt.Sprintf("unknown provider while beginning to initialize state store %q from provider %q", + root.StateStore.Type, + root.StateStore.Provider.Name)) + } + + var exists bool + factory, exists := ctxOpts.Providers[root.StateStore.ProviderAddr] + if !exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider unavailable", + Detail: fmt.Sprintf("The provider %s (%q) is required to initialize the %q state store, but the matching provider factory is missing. This is a bug in Terraform and should be reported.", + root.StateStore.Provider.Name, + root.StateStore.ProviderAddr, + root.StateStore.Type, + ), + Subject: &root.Backend.TypeRange, + }) + return nil, true, diags + } + + // If overrides supplied by -backend-config CLI flag, process them + var configOverride hcl.Body + if !extraConfig.Empty() { + // We need to launch an instance of the provider to get the config of the state store for processing any overrides. + provider, err := factory() + defer provider.Close() // Stop the child process once we're done with it here. + if err != nil { + diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err)) + return nil, true, diags + } + + resp := provider.GetProviderSchema() - var backendConfig *configs.Backend - var backendConfigOverride hcl.Body - if root.Backend != nil { + if len(resp.StateStores) == 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider does not support pluggable state storage", + Detail: fmt.Sprintf("There are no state stores implemented by provider %s (%q)", + root.StateStore.Provider.Name, + root.StateStore.ProviderAddr), + Subject: &root.StateStore.DeclRange, + }) + return nil, true, diags + } + + stateStoreSchema, exists := resp.StateStores[root.StateStore.Type] + if !exists { + suggestions := slices.Sorted(maps.Keys(resp.StateStores)) + suggestion := didyoumean.NameSuggestion(root.StateStore.Type, suggestions) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "State store not implemented by the provider", + Detail: fmt.Sprintf("State store %q is not implemented by provider %s (%q)%s", + root.StateStore.Type, root.StateStore.Provider.Name, + root.StateStore.ProviderAddr, suggestion), + Subject: &root.StateStore.DeclRange, + }) + return nil, true, diags + } + + // Handle any overrides supplied via -backend-config CLI flags + var overrideDiags tfdiags.Diagnostics + configOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, stateStoreSchema.Body) + diags = diags.Append(overrideDiags) + if overrideDiags.HasErrors() { + return nil, true, diags + } + } + + opts = &BackendOpts{ + StateStoreConfig: root.StateStore, + ProviderFactory: factory, + ConfigOverride: configOverride, + Init: true, + ViewType: viewType, + } + + case root.Backend != nil: + // backend config present backendType := root.Backend.Type if backendType == "cloud" { diags = diags.Append(&hcl.Diagnostic{ @@ -456,15 +570,24 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext b := bf() backendSchema := b.ConfigSchema() - backendConfig = root.Backend + backendConfig := root.Backend - var overrideDiags tfdiags.Diagnostics - backendConfigOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, backendSchema) + backendConfigOverride, overrideDiags := c.backendConfigOverrideBody(extraConfig, backendSchema) diags = diags.Append(overrideDiags) if overrideDiags.HasErrors() { return nil, true, diags } - } else { + + opts = &BackendOpts{ + BackendConfig: backendConfig, + ConfigOverride: backendConfigOverride, + Init: true, + ViewType: viewType, + } + + default: + // No config; defaults to local state storage + // If the user supplied a -backend-config on the CLI but no backend // block was found in the configuration, it's likely - but not // necessarily - a mistake. Return a warning. @@ -486,14 +609,13 @@ However, if you intended to override a defined backend, please verify that the backend configuration is present and valid. `, )) + } - } - opts := &BackendOpts{ - BackendConfig: backendConfig, - ConfigOverride: backendConfigOverride, - Init: true, - ViewType: viewType, + opts = &BackendOpts{ + Init: true, + ViewType: viewType, + } } back, backDiags := c.Backend(opts) @@ -896,7 +1018,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, if !newLocks.Equal(previousLocks) { // if readonly mode if flagLockfile == "readonly" { - // check if required provider dependences change + // check if required provider dependencies change if !newLocks.EqualProviderAddress(previousLocks) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -1114,7 +1236,7 @@ Options: itself. -force-copy Suppress prompts about copying state data when - initializating a new state backend. This is + initializing a new state backend. This is equivalent to providing a "yes" to all confirmation prompts. diff --git a/internal/command/views/init.go b/internal/command/views/init.go index cd91daa428..9f7f784b22 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -186,6 +186,10 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe HumanValue: "\n[reset][bold]Initializing provider plugins...", JSONValue: "Initializing provider plugins...", }, + "initializing_state_store_message": { + HumanValue: "\n[reset][bold]Initializing the state store...", + JSONValue: "Initializing the state store...", + }, "dependencies_lock_changes_info": { HumanValue: dependenciesLockChangesInfo, JSONValue: dependenciesLockChangesInfo, @@ -258,6 +262,7 @@ const ( InitializingTerraformCloudMessage InitMessageCode = "initializing_terraform_cloud_message" InitializingModulesMessage InitMessageCode = "initializing_modules_message" InitializingBackendMessage InitMessageCode = "initializing_backend_message" + InitializingStateStoreMessage InitMessageCode = "initializing_state_store_message" InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" LockInfo InitMessageCode = "lock_info" DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info"