diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 6a9c461b89..8b3559e747 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -13,7 +13,9 @@ import ( "errors" "fmt" "log" + "maps" "path/filepath" + "slices" "strconv" "strings" @@ -31,7 +33,9 @@ import ( "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/command/workdir" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/didyoumean" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -43,6 +47,17 @@ type BackendOpts struct { // the root module, or nil if no such block is present. BackendConfig *configs.Backend + // StateStoreConfig is a representation of the state_store configuration block given in + // the root module, or nil if no such block is present. + StateStoreConfig *configs.StateStore + + // ProvidersFactory contains a factory for creating instances of the + // provider used for pluggable state storage. Each call created a new instance, + // so be conscious of when the provider needs to be configured, etc. + // + // This will only be set if the configuration contains a state_store block. + ProviderFactory providers.Factory + // 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. @@ -511,6 +526,90 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags. return &configCopy, configHash, diags } +// stateStoreConfig returns the local 'state_store' configuration +// This method: +// > Ensures that that state store type exists in the linked provider. +// > Returns config that is the combination of config and any config overrides originally supplied via the CLI. +// > Returns a hash of the config in the configuration files, i.e. excluding overrides +func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, int, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + c := opts.StateStoreConfig + if c == nil { + // We choose to not to re-parse the config to look for data if it's missing, + // which currently happens in the similar `backendConfig` method. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing state store configuration", + Detail: "Terraform attempted to configure a state store when no parsed 'state_store' configuration was present. This is a bug in Terraform and should be reported.", + }) + return nil, 0, 0, diags + } + + // Check - is the state store type in the config supported by the provider? + provider, err := opts.ProviderFactory() + if err != nil { + diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err)) + return nil, 0, 0, diags + } + defer provider.Close() // Stop the child process once we're done with it here. + + resp := provider.GetProviderSchema() + + 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)", + c.Provider.Name, + c.ProviderAddr), + Subject: &c.DeclRange, + }) + return nil, 0, 0, diags + } + + stateStoreSchema, exists := resp.StateStores[c.Type] + if !exists { + suggestions := slices.Sorted(maps.Keys(resp.StateStores)) + suggestion := didyoumean.NameSuggestion(c.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", + c.Type, c.Provider.Name, + c.ProviderAddr, suggestion), + Subject: &c.DeclRange, + }) + return nil, 0, 0, diags + } + + // We know that the provider contains a state store with the correct type name. + // Validation of the config against the schema happens later. + // For now, we: + // > Get a hash of the present config + // > Apply any overrides + + configBody := c.Config + stateStoreHash, providerHash, diags := c.Hash(stateStoreSchema.Body, resp.Provider.Body) + + // If we have an override configuration body then we must apply it now. + if opts.ConfigOverride != nil { + log.Println("[TRACE] Meta.Backend: merging -backend-config=... CLI overrides into state_store configuration") + configBody = configs.MergeBodies(configBody, opts.ConfigOverride) + } + + log.Printf("[TRACE] Meta.Backend: built configuration for %q state_store with hash value %d and nested provider block with hash value %d", c.Type, stateStoreHash, providerHash) + + // We'll shallow-copy configs.StateStore here so that we can replace the + // body without affecting others that hold this reference. + configCopy := *c + configCopy.Config = configBody + return &configCopy, stateStoreHash, providerHash, diags +} + // backendFromConfig returns the initialized (not configured) backend // directly from the config/state.. // @@ -524,10 +623,29 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags. // 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, tfdiags.Diagnostics) { - // Get the local backend configuration. - c, cHash, diags := m.backendConfig(opts) - if diags.HasErrors() { - return nil, diags + var diags tfdiags.Diagnostics + + // Get the local 'backend' or 'state_store' configuration. + var backendConfig *configs.Backend + var stateStoreConfig *configs.StateStore + var cHash int + if opts.StateStoreConfig != nil { + // state store has been parsed from config and is included in opts + var ssDiags tfdiags.Diagnostics + stateStoreConfig, cHash, _, ssDiags = m.stateStoreConfig(opts) + diags = diags.Append(ssDiags) + if ssDiags.HasErrors() { + return nil, diags + } + } else { + // backend config may or may not have been parsed and included in opts, + // or may not exist in config at all (default/implied local backend) + var beDiags tfdiags.Diagnostics + backendConfig, cHash, beDiags = m.backendConfig(opts) + diags = diags.Append(beDiags) + if beDiags.HasErrors() { + return nil, diags + } } // ------------------------------------------------------------------------ @@ -564,15 +682,20 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di s = workdir.NewBackendStateFile() } else if s.Backend != nil { log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type) + } else if s.StateStore != nil { + log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q state_store using provider %q, version %s", + s.StateStore.Type, + s.StateStore.Provider.Source, + s.StateStore.Provider.Version) } else { log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend (is using legacy remote state?)") } - // if we want to force reconfiguration of the backend, we set the backend - // state to nil on this copy. This will direct us through the correct - // configuration path in the switch statement below. + // if we want to force reconfiguration of the backend or state store, we set the backend + // and state_store state to nil on this copy. This will direct us through the correct if m.reconfigure { s.Backend = nil + s.StateStore = nil } // Upon return, we want to set the state we're using in-memory so that @@ -588,12 +711,14 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // configuring new backends, updating previously-configured backends, etc. switch { // No configuration set at all. Pure local state. - case c == nil && s.Backend.Empty(): + case backendConfig == nil && s.Backend.Empty() && + stateStoreConfig == nil && s.StateStore.Empty(): log.Printf("[TRACE] Meta.Backend: using default local state only (no backend configuration, and no existing initialized backend)") return nil, nil // We're unsetting a backend (moving from backend => local) - case c == nil && !s.Backend.Empty(): + case backendConfig == nil && !s.Backend.Empty() && + stateStoreConfig == nil && s.StateStore.Empty(): log.Printf("[TRACE] Meta.Backend: previously-initialized %q backend is no longer present in config", s.Backend.Type) initReason := fmt.Sprintf("Unsetting the previously set backend %q", s.Backend.Type) @@ -611,13 +736,28 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return nil, diags } - return m.backend_c_r_S(c, cHash, sMgr, true, opts) + return m.backend_c_r_S(backendConfig, cHash, sMgr, true, opts) + + // We're unsetting a state_store (moving from state_store => local) + case stateStoreConfig == nil && !s.StateStore.Empty() && + backendConfig == nil && s.Backend.Empty(): + log.Printf("[TRACE] Meta.Backend: previously-initialized state_store %q in provider %s (%q) is no longer present in config", + s.StateStore.Type, + s.StateStore.Provider.Source.Type, + s.StateStore.Provider.Source, + ) + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Not implemented yet", + Detail: "Unsetting a state store is not implemented yet", + }) // Configuring a backend for the first time or -reconfigure flag was used - case c != nil && s.Backend.Empty(): - log.Printf("[TRACE] Meta.Backend: moving from default local state only to %q backend", c.Type) + case backendConfig != nil && s.Backend.Empty() && + stateStoreConfig == nil && s.StateStore.Empty(): + log.Printf("[TRACE] Meta.Backend: moving from default local state only to %q backend", backendConfig.Type) if !opts.Init { - if c.Type == "cloud" { + if backendConfig.Type == "cloud" { initReason := "Initial configuration of HCP Terraform or Terraform Enterprise" diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -625,7 +765,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason), )) } else { - initReason := fmt.Sprintf("Initial configuration of the requested backend %q", c.Type) + initReason := fmt.Sprintf("Initial configuration of the requested backend %q", backendConfig.Type) diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Backend initialization required, please run \"terraform init\"", @@ -634,16 +774,62 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } return nil, diags } - return m.backend_C_r_s(c, cHash, sMgr, opts) + return m.backend_C_r_s(backendConfig, cHash, sMgr, opts) + + // Configuring a state store for the first time or -reconfigure flag was used + case stateStoreConfig != nil && s.StateStore.Empty() && + backendConfig == nil && s.Backend.Empty(): + log.Printf("[TRACE] Meta.Backend: moving from default local state only to state_store %q in provider %s (%q)", + stateStoreConfig.Type, + stateStoreConfig.Provider.Name, + stateStoreConfig.ProviderAddr, + ) + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Not implemented yet", + Detail: "Configuring a state store for the first time is not implemented yet", + }) + + // Migration from state store to backend + case backendConfig != nil && s.Backend.Empty() && + stateStoreConfig == nil && !s.StateStore.Empty(): + log.Printf("[TRACE] Meta.Backend: config has changed from state_store %q in provider %s (%q) to backend %q", + s.StateStore.Type, + s.StateStore.Provider.Source.Type, + s.StateStore.Provider.Source, + backendConfig.Type, + ) + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Not implemented yet", + Detail: "Migration from state store to backend is not implemented yet", + }) + + // Migration from backend to state store + case backendConfig == nil && !s.Backend.Empty() && + stateStoreConfig != nil && s.StateStore.Empty(): + log.Printf("[TRACE] Meta.Backend: config has changed from backend %q to state_store %q in provider %s (%q)", + s.Backend.Type, + stateStoreConfig.Type, + stateStoreConfig.Provider.Name, + stateStoreConfig.ProviderAddr, + ) + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Not implemented yet", + Detail: "Migration from backend to state store is not implemented yet", + }) + // Potentially changing a backend configuration - case c != nil && !s.Backend.Empty(): + case backendConfig != nil && !s.Backend.Empty() && + stateStoreConfig == nil && s.StateStore.Empty(): // We are not going to migrate if... // // We're not initializing // AND the backend cache hash values match, indicating that the stored config is valid and completely unchanged. // AND we're not providing any overrides. An override can mean a change overriding an unchanged backend block (indicated by the hash value). if (uint64(cHash) == s.Backend.Hash) && (!opts.Init || opts.ConfigOverride == nil) { - log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q backend configuration", c.Type) + log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q backend configuration", backendConfig.Type) savedBackend, diags := m.savedBackend(sMgr) // Verify that selected workspace exist. Otherwise prompt user to create one if opts.Init && savedBackend != nil { @@ -659,8 +845,8 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // -backend-config options) is the same, then we're just initializing a previously // configured backend. The literal configuration may differ, however, so while we // don't need to migrate, we update the backend cache hash value. - if !m.backendConfigNeedsMigration(c, s.Backend) { - log.Printf("[TRACE] Meta.Backend: using already-initialized %q backend configuration", c.Type) + if !m.backendConfigNeedsMigration(backendConfig, s.Backend) { + log.Printf("[TRACE] Meta.Backend: using already-initialized %q backend configuration", backendConfig.Type) savedBackend, moreDiags := m.savedBackend(sMgr) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -684,13 +870,13 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return savedBackend, diags } - log.Printf("[TRACE] Meta.Backend: backend configuration has changed (from type %q to type %q)", s.Backend.Type, c.Type) + log.Printf("[TRACE] Meta.Backend: backend configuration has changed (from type %q to type %q)", s.Backend.Type, backendConfig.Type) - cloudMode := cloud.DetectConfigChangeType(s.Backend, c, false) + cloudMode := cloud.DetectConfigChangeType(s.Backend, backendConfig, false) if !opts.Init { //user ran another cmd that is not init but they are required to initialize because of a potential relevant change to their backend configuration - initDiag := m.determineInitReason(s.Backend.Type, c.Type, cloudMode) + initDiag := m.determineInitReason(s.Backend.Type, backendConfig.Type, cloudMode) diags = diags.Append(initDiag) return nil, diags } @@ -701,15 +887,39 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } log.Printf("[WARN] backend config has changed since last init") - return m.backend_C_r_S_changed(c, cHash, sMgr, true, opts) + return m.backend_C_r_S_changed(backendConfig, cHash, sMgr, true, opts) + + // Potentially changing a state store configuration + case backendConfig == nil && s.Backend.Empty() && + stateStoreConfig != nil && !s.StateStore.Empty(): + // When implemented, this will need to handle multiple scenarios like: + // > Changing to using a different provider for PSS. + // > Changing to using a different version of the same provider for PSS. + // >>>> Navigating state upgrades that do not force an explicit migration && + // identifying when migration is required. + // > Changing to using a different store in the same version of the provider. + // > Changing how the provider is configured. + // > Changing how the store is configured. + // > Allowing values to be moved between partial overrides and config + + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Not implemented yet", + Detail: "Changing a state store configuration is not implemented yet", + }) default: 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", - c == nil, s.Backend.Empty(), + "Backend Config Nil: %v\n"+ + "Saved Backend Empty: %v\n"+ + "StateStore Config Nil: %v\n"+ + "Saved StateStore Empty: %v\n", + backendConfig == nil, + s.Backend.Empty(), + stateStoreConfig == nil, + s.StateStore.Empty(), )) return nil, diags } diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 36dbdc513a..014b43d00c 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -19,8 +19,11 @@ import ( "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/workdir" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/copy" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -226,7 +229,7 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { } // Verify that interpolations result in an error -func TestMetaBackend_configureInterpolation(t *testing.T) { +func TestMetaBackend_configureBackendInterpolation(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-interp"), td) @@ -240,10 +243,14 @@ func TestMetaBackend_configureInterpolation(t *testing.T) { if err == nil { t.Fatal("should error") } + wantErr := "Variables not allowed" + if !strings.Contains(err.Err().Error(), wantErr) { + t.Fatalf("error should include %q, got: %s", wantErr, err.Err()) + } } // Newly configured backend -func TestMetaBackend_configureNew(t *testing.T) { +func TestMetaBackend_configureNewBackend(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath("backend-new"), td) t.Chdir(td) @@ -306,7 +313,7 @@ func TestMetaBackend_configureNew(t *testing.T) { } // Newly configured backend with prior local state and no remote state -func TestMetaBackend_configureNewWithState(t *testing.T) { +func TestMetaBackend_configureNewBackendWithState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate"), td) @@ -383,7 +390,7 @@ func TestMetaBackend_configureNewWithState(t *testing.T) { // Newly configured backend with matching local and remote state doesn't prompt // for copy. -func TestMetaBackend_configureNewWithoutCopy(t *testing.T) { +func TestMetaBackend_configureNewBackendWithoutCopy(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate"), td) @@ -433,7 +440,7 @@ func TestMetaBackend_configureNewWithoutCopy(t *testing.T) { // Newly configured backend with prior local state and no remote state, // but opting to not migrate. -func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) { +func TestMetaBackend_configureNewBackendWithStateNoMigrate(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate"), td) @@ -477,7 +484,7 @@ func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) { } // Newly configured backend with prior local state and remote state -func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { +func TestMetaBackend_configureNewBackendWithStateExisting(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate-existing"), td) @@ -548,7 +555,7 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { } // Newly configured backend with prior local state and remote state -func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { +func TestMetaBackend_configureNewBackendWithStateExistingNoMigrate(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-new-migrate-existing"), td) @@ -661,7 +668,7 @@ func TestMetaBackend_configuredUnchanged(t *testing.T) { } // Changing a configured backend -func TestMetaBackend_configuredChange(t *testing.T) { +func TestMetaBackend_changeConfiguredBackend(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change"), td) @@ -740,7 +747,7 @@ func TestMetaBackend_configuredChange(t *testing.T) { // Reconfiguring with an already configured backend. // This should ignore the existing backend config, and configure the new // backend is if this is the first time. -func TestMetaBackend_reconfigureChange(t *testing.T) { +func TestMetaBackend_reconfigureBackendChange(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-single-to-single"), td) @@ -793,7 +800,7 @@ func TestMetaBackend_reconfigureChange(t *testing.T) { // the currently selected workspace should prompt the user with a list of // workspaces to choose from to select a valid one, if more than one workspace // is available. -func TestMetaBackend_initSelectedWorkspaceDoesNotExist(t *testing.T) { +func TestMetaBackend_initBackendSelectedWorkspaceDoesNotExist(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-multi"), td) @@ -826,7 +833,7 @@ func TestMetaBackend_initSelectedWorkspaceDoesNotExist(t *testing.T) { // Initializing a backend which supports workspaces and does *not* have the // currently selected workspace - and which only has a single workspace - should // automatically select that single workspace. -func TestMetaBackend_initSelectedWorkspaceDoesNotExistAutoSelect(t *testing.T) { +func TestMetaBackend_initBackendSelectedWorkspaceDoesNotExistAutoSelect(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-single"), td) @@ -867,7 +874,7 @@ func TestMetaBackend_initSelectedWorkspaceDoesNotExistAutoSelect(t *testing.T) { // Initializing a backend which supports workspaces and does *not* have // the currently selected workspace with input=false should fail. -func TestMetaBackend_initSelectedWorkspaceDoesNotExistInputFalse(t *testing.T) { +func TestMetaBackend_initBackendSelectedWorkspaceDoesNotExistInputFalse(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-multi"), td) @@ -887,7 +894,7 @@ func TestMetaBackend_initSelectedWorkspaceDoesNotExistInputFalse(t *testing.T) { } // Changing a configured backend, copying state -func TestMetaBackend_configuredChangeCopy(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change"), td) @@ -934,7 +941,7 @@ func TestMetaBackend_configuredChangeCopy(t *testing.T) { // Changing a configured backend that supports only single states to another // backend that only supports single states. -func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy_singleState(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-single-to-single"), td) @@ -988,7 +995,7 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) { // Changing a configured backend that supports multi-state to a // backend that only supports single states. The multi-state only has // a default state. -func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy_multiToSingleDefault(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-default-to-single"), td) @@ -1041,7 +1048,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) { // Changing a configured backend that supports multi-state to a // backend that only supports single states. -func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy_multiToSingle(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-single"), td) @@ -1110,7 +1117,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) { // Changing a configured backend that supports multi-state to a // backend that only supports single states. -func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy_multiToSingleCurrentEnv(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-single"), td) @@ -1175,7 +1182,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) // Changing a configured backend that supports multi-state to a // backend that also supports multi-state. -func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy_multiToMulti(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-multi"), td) @@ -1268,7 +1275,7 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { // Changing a configured backend that supports multi-state to a // backend that also supports multi-state, but doesn't allow a // default state while the default state is non-empty. -func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy_multiToNoDefaultWithDefault(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-no-default-with-default"), td) @@ -1343,7 +1350,7 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing // Changing a configured backend that supports multi-state to a // backend that also supports multi-state, but doesn't allow a // default state while the default state is empty. -func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *testing.T) { +func TestMetaBackend_configuredBackendChangeCopy_multiToNoDefaultWithoutDefault(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-change-multi-to-no-default-without-default"), td) @@ -1415,7 +1422,7 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *test } // Unsetting a saved backend -func TestMetaBackend_configuredUnset(t *testing.T) { +func TestMetaBackend_configuredBackendUnset(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-unset"), td) @@ -1477,7 +1484,7 @@ func TestMetaBackend_configuredUnset(t *testing.T) { } // Unsetting a saved backend and copying the remote state -func TestMetaBackend_configuredUnsetCopy(t *testing.T) { +func TestMetaBackend_configuredBackendUnsetCopy(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("backend-unset"), td) @@ -1811,7 +1818,7 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { } // init a backend using -backend-config options multiple times -func TestMetaBackend_configureWithExtra(t *testing.T) { +func TestMetaBackend_configureBackendWithExtra(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend-empty"), td) @@ -1892,7 +1899,7 @@ func TestMetaBackend_localDoesNotDeleteLocal(t *testing.T) { } // move options from config to -backend-config -func TestMetaBackend_configToExtra(t *testing.T) { +func TestMetaBackend_backendConfigToExtra(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() testCopyDir(t, testFixturePath("init-backend"), td) @@ -2060,6 +2067,452 @@ func Test_determineInitReason(t *testing.T) { } } +// Newly configured state store +// +// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch +// case for this scenario, and will need to be updated when that init feature is implemented. +func TestMetaBackend_configureNewStateStore(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-new"), td) + t.Chdir(td) + + // Setup the meta + m := testMetaBackend(t, nil) + m.AllowExperimentalFeatures = true + + // Get the state store's config + mod, loadDiags := m.loadSingleModule(td) + if loadDiags.HasErrors() { + t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) + } + + // Get mock provider factory to be used during init + // + // This imagines a provider called foo that contains + // a pluggable state store implementation called bar. + mock := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": {Type: cty.String, Optional: true}, + }, + }, + }, + DataSources: map[string]providers.Schema{}, + ResourceTypes: map[string]providers.Schema{}, + ListResourceTypes: map[string]providers.Schema{}, + StateStores: map[string]providers.Schema{ + "foo_bar": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + factory := func() (providers.Interface, error) { + return mock, nil + } + + // Get the operations backend + _, beDiags := m.Backend(&BackendOpts{ + Init: true, + StateStoreConfig: mod.StateStore, + ProviderFactory: factory, + }) + if !beDiags.HasErrors() { + t.Fatal("expected an error to be returned during partial implementation of PSS") + } + wantErr := "Configuring a state store for the first time is not implemented yet" + if !strings.Contains(beDiags.Err().Error(), wantErr) { + t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) + } + +} + +// Unsetting a saved state store +// +// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch +// case for this scenario, and will need to be updated when that init feature is implemented. +func TestMetaBackend_configuredStateStoreUnset(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-unset"), td) + t.Chdir(td) + + // Setup the meta + m := testMetaBackend(t, nil) + m.AllowExperimentalFeatures = true + + // Get the state store's config + mod, loadDiags := m.loadSingleModule(td) + if loadDiags.HasErrors() { + t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) + } + + // No mock provider is used here - yet + // Logic will need to be implemented that lets the init have access to + // a factory for the 'old' provider used for PSS previously. This will be + // used when migrating away from PSS entirely, or to a new PSS configuration. + + // Get the operations backend + _, beDiags := m.Backend(&BackendOpts{ + Init: true, + StateStoreConfig: mod.StateStore, + }) + if !beDiags.HasErrors() { + t.Fatal("expected an error to be returned during partial implementation of PSS") + } + wantErr := "Unsetting a state store is not implemented yet" + if !strings.Contains(beDiags.Err().Error(), wantErr) { + t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) + } +} + +// Reconfiguring with an already configured state store. +// This should ignore the existing state_store config, and configure the new +// state store is if this is the first time. +// +// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch +// case for this scenario, and will need to be updated when that init feature is implemented. +func TestMetaBackend_reconfigureStateStoreChange(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-reconfigure"), td) + t.Chdir(td) + + // Setup the meta + m := testMetaBackend(t, nil) + m.AllowExperimentalFeatures = true + + // this should not ask for input + m.input = false + + // cli flag -reconfigure + m.reconfigure = true + + // Get the state store's config + mod, loadDiags := m.loadSingleModule(td) + if loadDiags.HasErrors() { + t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) + } + + // Get mock provider factory to be used during init + // + // This imagines a provider called foo that contains + // a pluggable state store implementation called bar. + mock := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": {Type: cty.String, Optional: true}, + }, + }, + }, + DataSources: map[string]providers.Schema{}, + ResourceTypes: map[string]providers.Schema{}, + ListResourceTypes: map[string]providers.Schema{}, + StateStores: map[string]providers.Schema{ + "foo_bar": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + factory := func() (providers.Interface, error) { + return mock, nil + } + + // Get the operations backend + _, beDiags := m.Backend(&BackendOpts{ + Init: true, + StateStoreConfig: mod.StateStore, + ProviderFactory: factory, + }) + + if !beDiags.HasErrors() { + t.Fatal("expected an error to be returned during partial implementation of PSS") + } + wantErr := "Configuring a state store for the first time is not implemented yet" + if !strings.Contains(beDiags.Err().Error(), wantErr) { + t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) + } + +} + +// Changing a configured state store +// +// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch +// case for this scenario, and will need to be updated when that init feature is implemented. +// ALSO, this test will need to be split into multiple scenarios in future. +func TestMetaBackend_changeConfiguredStateStore(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-changed"), td) + t.Chdir(td) + + // Setup the meta + m := testMetaBackend(t, nil) + m.AllowExperimentalFeatures = true + + // Get the state store's config + mod, loadDiags := m.loadSingleModule(td) + if loadDiags.HasErrors() { + t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) + } + + // Get mock provider factory to be used during init + // + // This imagines a provider called foo that contains + // a pluggable state store implementation called bar. + mock := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": {Type: cty.String, Optional: true}, + }, + }, + }, + DataSources: map[string]providers.Schema{}, + ResourceTypes: map[string]providers.Schema{}, + ListResourceTypes: map[string]providers.Schema{}, + StateStores: map[string]providers.Schema{ + "foo_bar": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + factory := func() (providers.Interface, error) { + return mock, nil + } + + // Get the operations backend + _, beDiags := m.Backend(&BackendOpts{ + Init: true, + StateStoreConfig: mod.StateStore, + ProviderFactory: factory, + }) + if !beDiags.HasErrors() { + t.Fatal("expected an error to be returned during partial implementation of PSS") + } + wantErr := "Changing a state store configuration is not implemented yet" + if !strings.Contains(beDiags.Err().Error(), wantErr) { + t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) + } +} + +// Changing from using backend to state_store +// +// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch +// case for this scenario, and will need to be updated when that init feature is implemented. +func TestMetaBackend_configuredBackendToStateStore(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("backend-to-state-store"), td) + t.Chdir(td) + + // Setup the meta + m := testMetaBackend(t, nil) + m.AllowExperimentalFeatures = true + + // Get the state store's config + mod, loadDiags := m.loadSingleModule(td) + if loadDiags.HasErrors() { + t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) + } + + // Get mock provider factory to be used during init + // + // This imagines a provider called foo that contains + // a pluggable state store implementation called bar. + mock := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": {Type: cty.String, Optional: true}, + }, + }, + }, + DataSources: map[string]providers.Schema{}, + ResourceTypes: map[string]providers.Schema{}, + ListResourceTypes: map[string]providers.Schema{}, + StateStores: map[string]providers.Schema{ + "foo_bar": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + factory := func() (providers.Interface, error) { + return mock, nil + } + + // Get the operations backend + _, beDiags := m.Backend(&BackendOpts{ + Init: true, + StateStoreConfig: mod.StateStore, + ProviderFactory: factory, + }) + if !beDiags.HasErrors() { + t.Fatal("expected an error to be returned during partial implementation of PSS") + } + wantErr := "Migration from backend to state store is not implemented yet" + if !strings.Contains(beDiags.Err().Error(), wantErr) { + t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) + } +} + +// Changing from using state_store to backend +// +// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch +// case for this scenario, and will need to be updated when that init feature is implemented. +func TestMetaBackend_configuredStateStoreToBackend(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-to-backend"), td) + t.Chdir(td) + + // Setup the meta + m := testMetaBackend(t, nil) + m.AllowExperimentalFeatures = true + + // Get the backend's config + mod, loadDiags := m.loadSingleModule(td) + if loadDiags.HasErrors() { + t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) + } + + // No mock provider is used here - yet + // Logic will need to be implemented that lets the init have access to + // a factory for the 'old' provider used for PSS previously. This will be + // used when migrating away from PSS entirely, or to a new PSS configuration. + + // Get the operations backend + _, beDiags := m.Backend(&BackendOpts{ + Init: true, + BackendConfig: mod.Backend, + }) + if !beDiags.HasErrors() { + t.Fatal("expected an error to be returned during partial implementation of PSS") + } + wantErr := "Migration from state store to backend is not implemented yet" + if !strings.Contains(beDiags.Err().Error(), wantErr) { + t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) + } +} + +// Verify that using variables results in an error +func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) { + wantErr := "Variables not allowed" + + cases := map[string]struct { + fixture string + wantErr string + }{ + "no variables in nested provider block": { + fixture: "state-store-new-vars-in-provider", + wantErr: wantErr, + }, + "no variables in the state_store block": { + fixture: "state-store-new-vars-in-store", + wantErr: wantErr, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath(tc.fixture), td) + t.Chdir(td) + + // Setup the meta + m := testMetaBackend(t, nil) + m.AllowExperimentalFeatures = true + + // Get the state store's config + mod, loadDiags := m.loadSingleModule(td) + if loadDiags.HasErrors() { + t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) + } + + // Get mock provider factory to be used during init + // + // This imagines a provider called foo that contains + // a pluggable state store implementation called bar. + mock := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": {Type: cty.String, Optional: true}, + }, + }, + }, + DataSources: map[string]providers.Schema{}, + ResourceTypes: map[string]providers.Schema{}, + ListResourceTypes: map[string]providers.Schema{}, + StateStores: map[string]providers.Schema{ + "foo_bar": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + factory := func() (providers.Interface, error) { + return mock, nil + } + + // Get the operations backend + _, err := m.Backend(&BackendOpts{ + Init: true, + StateStoreConfig: mod.StateStore, + ProviderFactory: factory, + }) + if err == nil { + t.Fatal("should error") + } + if !strings.Contains(err.Err().Error(), tc.wantErr) { + t.Fatalf("error should include %q, got: %s", tc.wantErr, err.Err()) + } + + }) + } +} + func testMetaBackend(t *testing.T, args []string) *Meta { var m Meta m.Ui = new(cli.MockUi) diff --git a/internal/command/testdata/backend-to-state-store/.terraform/terraform.tfstate b/internal/command/testdata/backend-to-state-store/.terraform/terraform.tfstate new file mode 100644 index 0000000000..c7e7f5f455 --- /dev/null +++ b/internal/command/testdata/backend-to-state-store/.terraform/terraform.tfstate @@ -0,0 +1,13 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": "local-state.tfstate", + "workspace_dir": null + }, + "hash": 4282859327 + } +} \ No newline at end of file diff --git a/internal/command/testdata/backend-to-state-store/main.tf b/internal/command/testdata/backend-to-state-store/main.tf new file mode 100644 index 0000000000..72643dbb95 --- /dev/null +++ b/internal/command/testdata/backend-to-state-store/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + state_store "foo_bar" { + provider "foo" {} + + bar = "foobar" + } +} diff --git a/internal/command/testdata/state-store-changed/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/.terraform/terraform.tfstate new file mode 100644 index 0000000000..a59e231faa --- /dev/null +++ b/internal/command/testdata/state-store-changed/.terraform/terraform.tfstate @@ -0,0 +1,18 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "foo_bar", + "config": { + "bar": "old-value" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/my-org/foo", + "config": {}, + "hash": 12345 + }, + "hash": 12345 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/main.tf b/internal/command/testdata/state-store-changed/main.tf new file mode 100644 index 0000000000..49184a175b --- /dev/null +++ b/internal/command/testdata/state-store-changed/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + state_store "foo_bar" { + provider "foo" {} + + bar = "changed-value" # changed versus backend state file + } +} diff --git a/internal/command/testdata/state-store-new-vars-in-provider/main.tf b/internal/command/testdata/state-store-new-vars-in-provider/main.tf new file mode 100644 index 0000000000..a779f50817 --- /dev/null +++ b/internal/command/testdata/state-store-new-vars-in-provider/main.tf @@ -0,0 +1,16 @@ +variable "foo" { default = "bar" } + +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + state_store "foo_bar" { + provider "foo" { + region = var.foo + } + + bar = "hardcoded" + } +} diff --git a/internal/command/testdata/state-store-new-vars-in-store/main.tf b/internal/command/testdata/state-store-new-vars-in-store/main.tf new file mode 100644 index 0000000000..31508d670b --- /dev/null +++ b/internal/command/testdata/state-store-new-vars-in-store/main.tf @@ -0,0 +1,16 @@ +variable "foo" { default = "bar" } + +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + state_store "foo_bar" { + provider "foo" { + region = "hardcoded" + } + + bar = var.foo + } +} diff --git a/internal/command/testdata/state-store-new/main.tf b/internal/command/testdata/state-store-new/main.tf new file mode 100644 index 0000000000..72643dbb95 --- /dev/null +++ b/internal/command/testdata/state-store-new/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + state_store "foo_bar" { + provider "foo" {} + + bar = "foobar" + } +} diff --git a/internal/command/testdata/state-store-reconfigure/.terraform/terraform.tfstate b/internal/command/testdata/state-store-reconfigure/.terraform/terraform.tfstate new file mode 100644 index 0000000000..a59e231faa --- /dev/null +++ b/internal/command/testdata/state-store-reconfigure/.terraform/terraform.tfstate @@ -0,0 +1,18 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "foo_bar", + "config": { + "bar": "old-value" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/my-org/foo", + "config": {}, + "hash": 12345 + }, + "hash": 12345 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-reconfigure/main.tf b/internal/command/testdata/state-store-reconfigure/main.tf new file mode 100644 index 0000000000..49184a175b --- /dev/null +++ b/internal/command/testdata/state-store-reconfigure/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + state_store "foo_bar" { + provider "foo" {} + + bar = "changed-value" # changed versus backend state file + } +} diff --git a/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate b/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate new file mode 100644 index 0000000000..d4b311d07d --- /dev/null +++ b/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate @@ -0,0 +1,18 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "foo_bar", + "config": { + "bar": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/my-org/foo", + "config": {}, + "hash": 12345 + }, + "hash": 12345 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-to-backend/main.tf b/internal/command/testdata/state-store-to-backend/main.tf new file mode 100644 index 0000000000..f0b7118c31 --- /dev/null +++ b/internal/command/testdata/state-store-to-backend/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + + # Config has been updated to use backend + # but a state_store block is still represented + # in the backend state file + backend "local" { + path = "local-state.tfstate" + } +} diff --git a/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate b/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate new file mode 100644 index 0000000000..d4b311d07d --- /dev/null +++ b/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate @@ -0,0 +1,18 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "foo_bar", + "config": { + "bar": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/my-org/foo", + "config": {}, + "hash": 12345 + }, + "hash": 12345 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-unset/main.tf b/internal/command/testdata/state-store-unset/main.tf new file mode 100644 index 0000000000..f35c0bbc0b --- /dev/null +++ b/internal/command/testdata/state-store-unset/main.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + foo = { + source = "my-org/foo" + } + } + + # Config has been updated to remove a state_store block +} diff --git a/internal/configs/config.go b/internal/configs/config.go index ce07d965c0..7b74726510 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -673,6 +673,26 @@ func (c *Config) resolveProviderTypes() map[string]addrs.Provider { return providers } +// resolveStateStoreProviderType gets tfaddr.Provider data for the provider used for pluggable state storage +// and assigns it to the ProviderAddr field in the config's root module's state store data. +// +// See the reused function resolveStateStoreProviderType for details about logic. +// If no match is found, an error diagnostic is returned. +func (c *Config) resolveStateStoreProviderType() hcl.Diagnostics { + var diags hcl.Diagnostics + + providerType, typeDiags := resolveStateStoreProviderType(c.Root.Module.ProviderRequirements.RequiredProviders, + *c.Root.Module.StateStore) + + if typeDiags.HasErrors() { + diags = append(diags, typeDiags...) + return diags + } + + c.Root.Module.StateStore.ProviderAddr = providerType + return nil +} + // resolveProviderTypesForTests matches resolveProviderTypes except it uses // the information from resolveProviderTypes to resolve the provider types for // providers defined within the configs test files. diff --git a/internal/configs/config_build.go b/internal/configs/config_build.go index 1f8d1788ad..e4b28c6c4f 100644 --- a/internal/configs/config_build.go +++ b/internal/configs/config_build.go @@ -41,6 +41,11 @@ func BuildConfig(root *Module, walker ModuleWalker, loader MockDataLoader) (*Con // the known types for validation. providers := cfg.resolveProviderTypes() cfg.resolveProviderTypesForTests(providers) + + if cfg.Module.StateStore != nil { + stateProviderDiags := cfg.resolveStateStoreProviderType() + diags = append(diags, stateProviderDiags...) + } } diags = append(diags, validateProviderConfigs(nil, cfg, nil)...) diff --git a/internal/configs/module.go b/internal/configs/module.go index 6c87279848..374f7f13ad 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -190,6 +190,10 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) { // Generate the FQN -> LocalProviderName map mod.gatherProviderLocalNames() + if mod.StateStore != nil { + diags = append(diags, mod.resolveStateStoreProviderType()...) + } + return mod, diags } @@ -891,6 +895,26 @@ func (m *Module) gatherProviderLocalNames() { m.ProviderLocalNames = providers } +// resolveStateStoreProviderType uses the processed module to get tfaddr.Provider data for the provider +// used for pluggable state storage, and assigns it to the ProviderAddr field in the module's state store data. +// +// See the reused function resolveStateStoreProviderType for details about logic. +// If no match is found, an error diagnostic is returned. +func (m *Module) resolveStateStoreProviderType() hcl.Diagnostics { + var diags hcl.Diagnostics + + providerType, typeDiags := resolveStateStoreProviderType(m.ProviderRequirements.RequiredProviders, + *m.StateStore) + + if typeDiags.HasErrors() { + diags = append(diags, typeDiags...) + return diags + } + + m.StateStore.ProviderAddr = providerType + return diags +} + // LocalNameForProvider returns the module-specific user-supplied local name for // a given provider FQN, or the default local name if none was supplied. func (m *Module) LocalNameForProvider(p addrs.Provider) string { diff --git a/internal/configs/module_test.go b/internal/configs/module_test.go index 38af3f71ac..c4fc7b4713 100644 --- a/internal/configs/module_test.go +++ b/internal/configs/module_test.go @@ -542,6 +542,7 @@ func TestModule_conflicting_backend_cloud_stateStore(t *testing.T) { t.Run(tc.dir, func(t *testing.T) { var diags hcl.Diagnostics if tc.allowExperiments { + // TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA. _, diags = testModuleFromDirWithExperiments(tc.dir) } else { _, diags = testModuleFromDir(tc.dir) @@ -559,7 +560,8 @@ func TestModule_conflicting_backend_cloud_stateStore(t *testing.T) { func TestModule_stateStore_overrides_stateStore(t *testing.T) { t.Run("it can override a state_store block with a different state_store block", func(t *testing.T) { - mod, diags := testModuleFromDir("testdata/valid-modules/override-state-store") + // TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA. + mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-state-store") if diags.HasErrors() { t.Fatal(diags.Error()) } @@ -598,7 +600,8 @@ func TestModule_stateStore_overrides_stateStore(t *testing.T) { // configuration file, as an omitted backend there implies the local backend. func TestModule_stateStore_override_no_base(t *testing.T) { t.Run("it can introduce a state_store block via overrides when the base config has has no cloud, backend, or state_store blocks", func(t *testing.T) { - mod, diags := testModuleFromDir("testdata/valid-modules/override-state-store-no-base") + // TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA. + mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-state-store-no-base") if diags.HasErrors() { t.Fatal(diags.Error()) } @@ -611,7 +614,8 @@ func TestModule_stateStore_override_no_base(t *testing.T) { func TestModule_stateStore_overrides_backend(t *testing.T) { t.Run("it can override a backend block with a state_store block", func(t *testing.T) { - mod, diags := testModuleFromDir("testdata/valid-modules/override-backend-with-state-store") + // TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA. + mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-backend-with-state-store") if diags.HasErrors() { t.Fatal(diags.Error()) } @@ -638,7 +642,8 @@ func TestModule_stateStore_overrides_backend(t *testing.T) { func TestModule_stateStore_overrides_cloud(t *testing.T) { t.Run("it can override a cloud block with a state_store block", func(t *testing.T) { - mod, diags := testModuleFromDir("testdata/valid-modules/override-cloud-with-state-store") + // TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA. + mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-cloud-with-state-store") if diags.HasErrors() { t.Fatal(diags.Error()) } @@ -664,8 +669,8 @@ func TestModule_stateStore_overrides_cloud(t *testing.T) { func TestModule_state_store_multiple(t *testing.T) { t.Run("it detects when two state_store blocks are present within the same module in separate files", func(t *testing.T) { - - _, diags := testModuleFromDir("testdata/invalid-modules/multiple-state-store") + // TODO(SarahFrench/radeksimko) - disable experiments in this test once the feature is GA. + _, diags := testModuleFromDirWithExperiments("testdata/invalid-modules/multiple-state-store") if !diags.HasErrors() { t.Fatal("module should have error diags, but does not") } diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index ce214c80e9..fc519acab9 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -35,6 +35,14 @@ func TestParserLoadConfigDirSuccess(t *testing.T) { name := info.Name() t.Run(name, func(t *testing.T) { parser := NewParser(nil) + + if strings.Contains(name, "state-store") { + // The PSS project is currently gated as experimental + // TODO(SarahFrench/radeksimko) - remove this from the test once + // the feature is GA. + parser.allowExperiments = true + } + path := filepath.Join("testdata/valid-modules", name) mod, diags := parser.LoadConfigDir(path) diff --git a/internal/configs/state_store.go b/internal/configs/state_store.go index a5b2fe93bc..aa12509095 100644 --- a/internal/configs/state_store.go +++ b/internal/configs/state_store.go @@ -4,8 +4,12 @@ package configs import ( + "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" @@ -22,6 +26,10 @@ type StateStore struct { Config hcl.Body Provider *Provider + // ProviderAddr contains the FQN of the provider used for pluggable state storage. + // This is required for accessing provider factories during Terraform command logic, + // and is used in diagnostics + ProviderAddr tfaddr.Provider TypeRange hcl.Range DeclRange hcl.Range @@ -74,6 +82,8 @@ func decodeStateStoreBlock(block *hcl.Block) (*StateStore, hcl.Diagnostics) { } ss.Provider = provider + // We cannot set a value for ss.ProviderAddr at this point. Instead, this is done later when the + // config has been parsed into a Config or Module and required_providers data is available. return ss, diags } @@ -87,6 +97,36 @@ var StateStorageBlockSchema = &hcl.BodySchema{ }, } +// resolveStateStoreProviderType is used to obtain provider source data from required_providers data. +// The only exception is the builtin terraform provider, which we return source data for without using required_providers. +// This code is reused in code for parsing config and modules. +func resolveStateStoreProviderType(requiredProviders map[string]*RequiredProvider, stateStore StateStore) (tfaddr.Provider, hcl.Diagnostics) { + var diags hcl.Diagnostics + + // We intentionally don't look for entries in required_providers under different local names and match them + // Users should use the same local name in the nested provider block as in required_providers. + addr, foundReqProviderEntry := requiredProviders[stateStore.Provider.Name] + switch { + case !foundReqProviderEntry && stateStore.Provider.Name == "terraform": + // We do not expect users to include built in providers in required_providers + // So, if we don't find an entry in required_providers under local name 'terraform' we assume + // that the builtin provider is intended. + return addrs.NewBuiltInProvider("terraform"), nil + case !foundReqProviderEntry: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing entry in required_providers", + Detail: fmt.Sprintf("The provider used for state storage must have a matching entry in required_providers. Please add an entry for provider %q", + stateStore.Provider.Name), + Subject: &stateStore.DeclRange, + }) + return tfaddr.Provider{}, diags + default: + // We've got a required_providers entry to use + return addr.Type, nil + } +} + // Hash produces a hash value for the receiver that covers the type and the // portions of the config that conform to the state_store schema. The provider // block that is nested inside state_store is ignored. diff --git a/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf b/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf index cb9d576e09..cc6a1222d0 100644 --- a/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf +++ b/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf @@ -1,5 +1,9 @@ terraform { - // Note: not valid config - a paired entry in required_providers is usually needed + required_providers { + foo = { + source = "my-org/foo" + } + } state_store "foo_override" { provider "foo" {} diff --git a/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf b/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf index d684746911..cc6a1222d0 100644 --- a/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf +++ b/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf @@ -1,10 +1,12 @@ terraform { - // Note: not valid config - a paired entry in required_providers is usually needed + required_providers { + foo = { + source = "my-org/foo" + } + } state_store "foo_override" { provider "foo" {} custom_attr = "override" } } - -provider "bar" {} diff --git a/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf b/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf index 1775231b41..00f3172f15 100644 --- a/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf +++ b/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf @@ -1,5 +1,9 @@ terraform { - // Note: not valid config - a paired entry in required_providers is usually needed + required_providers { + foo = { + source = "my-org/foo" + } + } state_store "foo_bar" { provider "foo" {} diff --git a/internal/configs/testdata/valid-modules/override-state-store/main.tf b/internal/configs/testdata/valid-modules/override-state-store/main.tf index 862a56ac4f..f5fa4c8369 100644 --- a/internal/configs/testdata/valid-modules/override-state-store/main.tf +++ b/internal/configs/testdata/valid-modules/override-state-store/main.tf @@ -1,5 +1,9 @@ terraform { - // Note: not valid config - a paired entry in required_providers is usually needed + required_providers { + foo = { + source = "my-org/foo" + } + } state_store "foo_bar" { provider "foo" {} diff --git a/internal/configs/testdata/valid-modules/override-state-store/override.tf b/internal/configs/testdata/valid-modules/override-state-store/override.tf index 9ee1a566db..f305dd5e74 100644 --- a/internal/configs/testdata/valid-modules/override-state-store/override.tf +++ b/internal/configs/testdata/valid-modules/override-state-store/override.tf @@ -1,5 +1,9 @@ terraform { - // Note: not valid config - a paired entry in required_providers is usually needed + required_providers { + bar = { + source = "my-org/bar" + } + } state_store "foo_override" { provider "bar" {}