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`
This commit is contained in:
Sarah French 2025-07-18 14:08:18 +01:00 committed by GitHub
parent ba361f2f5c
commit d1e412fcf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1034 additions and 63 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -0,0 +1,12 @@
terraform {
required_providers {
foo = {
source = "my-org/foo"
}
}
state_store "foo_bar" {
provider "foo" {}
bar = "foobar"
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,12 @@
terraform {
required_providers {
foo = {
source = "my-org/foo"
}
}
state_store "foo_bar" {
provider "foo" {}
bar = "foobar"
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,9 @@
terraform {
required_providers {
foo = {
source = "my-org/foo"
}
}
# Config has been updated to remove a state_store block
}

View file

@ -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.

View file

@ -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)...)

View file

@ -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 {

View file

@ -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")
}

View file

@ -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)

View file

@ -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.

View file

@ -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" {}

View file

@ -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" {}

View file

@ -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" {}

View file

@ -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" {}

View file

@ -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" {}