From d1e412fcf0eb29be0fe9f433f756466067e424f4 Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:08:18 +0100 Subject: [PATCH] PSS: Add initial (incomplete) version of code changes to the `init` command for using pluggable state storage (#37321) * Store the FQN of the provider used in PSS in representations of the parsed config. This can only be done once modules have been parsed and the required providers data is available. There are multiple places where config is parsed, into either Config or Module structs, so this needs to be implemented in multiple places. * Update affected tests, improve error diagnostic * Begin enabling method to return a backend.Backend made using state_store config. State store config can now be received via BackendOpts and there is rough validation of whether the config makes sense (does the provider offer a store with the given name?). * Update code's logic to include possibility of a state store being in use At this point there are no cases that actually handle the state store scenarios though! * Add empty cases for handle all broad init scenarios involving PSS * Update default case's error to report state store variables * Improve how we resolve the builtin terraform provider's address * Add test that hits the code path for adding a state store to a new (or implied local) project * Add test for use of `-reconfigure` flag; show that it hits the code path for adding a state store for the first time * Add test that hits the code path for removing use of a state store, migrating to (implied) local backend * Add test that hits the code path for changing a state store's configuration * Update existing test names to be backend-specific * Add tests that hits the code path for migrating between PSS and backends * Consolidate PSS-related tests at end of the file * Fix log text * Add test showing that using variables is disallowed with state_store and nested provider blocks * Update test name * Fix test cases * Add TODOs so we remember to remove experiments from tests * Update state store-related tests to use t.Cleanup * Remove use of `testChdir` --- internal/command/meta_backend.go | 262 ++++++++- internal/command/meta_backend_test.go | 501 +++++++++++++++++- .../.terraform/terraform.tfstate | 13 + .../testdata/backend-to-state-store/main.tf | 12 + .../.terraform/terraform.tfstate | 18 + .../testdata/state-store-changed/main.tf | 12 + .../state-store-new-vars-in-provider/main.tf | 16 + .../state-store-new-vars-in-store/main.tf | 16 + .../command/testdata/state-store-new/main.tf | 12 + .../.terraform/terraform.tfstate | 18 + .../testdata/state-store-reconfigure/main.tf | 12 + .../.terraform/terraform.tfstate | 18 + .../testdata/state-store-to-backend/main.tf | 14 + .../.terraform/terraform.tfstate | 18 + .../testdata/state-store-unset/main.tf | 9 + internal/configs/config.go | 20 + internal/configs/config_build.go | 5 + internal/configs/module.go | 24 + internal/configs/module_test.go | 17 +- internal/configs/parser_config_dir_test.go | 8 + internal/configs/state_store.go | 40 ++ .../override.tf | 6 +- .../override.tf | 8 +- .../override-state-store-no-base/override.tf | 6 +- .../override-state-store/main.tf | 6 +- .../override-state-store/override.tf | 6 +- 26 files changed, 1034 insertions(+), 63 deletions(-) create mode 100644 internal/command/testdata/backend-to-state-store/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/backend-to-state-store/main.tf create mode 100644 internal/command/testdata/state-store-changed/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-changed/main.tf create mode 100644 internal/command/testdata/state-store-new-vars-in-provider/main.tf create mode 100644 internal/command/testdata/state-store-new-vars-in-store/main.tf create mode 100644 internal/command/testdata/state-store-new/main.tf create mode 100644 internal/command/testdata/state-store-reconfigure/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-reconfigure/main.tf create mode 100644 internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-to-backend/main.tf create mode 100644 internal/command/testdata/state-store-unset/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-unset/main.tf 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" {}