Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions internal/command/e2etest/pluggable_state_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,141 @@ func TestPrimary_stateStore_providerCmds(t *testing.T) {
// This test just asserts that `terraform providers schema` can read state
// via the state store, and therefore detects all 3 providers needed for the output.
}

func TestInit_pluggableStateStore_providerUpgrade(t *testing.T) {
if !canRunGoBuild {
// We're running in a separate-build-then-run context, so we can't
// currently execute this test which depends on being able to build
// new executable at runtime.
//
// (See the comment on canRunGoBuild's declaration for more information.)
t.Skip("can't run without building a new provider executable")
}

t.Setenv(e2e.TestExperimentFlag, "true")
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
fixturePath := filepath.Join("testdata", "full-workflow-with-unconfigured-state-store-fs")
tf := e2e.NewBinary(t, terraformBin, fixturePath)

// Add a state file describing a resource from the simple (v5) provider, so
// we can test that the state is read and used to get all the provider schemas
fakeState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "simple_resource",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"bar"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("simple6"),
Module: addrs.RootModule,
},
)
})
fakeStateFile := &statefile.File{
Lineage: "boop",
Serial: 4,
TerraformVersion: version.Must(version.NewVersion("1.0.0")),
State: fakeState,
}
var fakeStateBuf bytes.Buffer
err := statefile.WriteForTest(fakeStateFile, &fakeStateBuf)
if err != nil {
t.Error(err)
}
fakeStateBytes := fakeStateBuf.Bytes()

oldStateDirName := "old"
newStateDirName := "new"

if err := os.MkdirAll(filepath.Join(tf.WorkDir(), oldStateDirName, "terraform.tfstate.d", "default"), os.ModePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(tf.WorkDir(), oldStateDirName, "terraform.tfstate.d", "default", "terraform.tfstate"), fakeStateBytes, 0644); err != nil {
t.Fatal(err)
}

platform := getproviders.CurrentPlatform.String()
cachePath := tf.Path("cache")
fsMirrorPath := filepath.Join(cachePath, "registry.terraform.io/hashicorp/simple6")

// build old version
oldProviderTmp := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
oldPath := filepath.Join(fsMirrorPath, "1.2.3", platform)
oldProviderExe := e2e.GoBuild(
"github.com/hashicorp/terraform/internal/provider-simple-v6/main", oldProviderTmp,
"-ldflags", "-X 'main.parentStateDir="+oldStateDirName+"'")
if err := os.MkdirAll(oldPath, os.ModePerm); err != nil {
t.Fatal(err)
}
targetPath := filepath.Join(oldPath, "terraform-provider-simple6")
if err := os.Rename(oldProviderExe, targetPath); err != nil {
t.Fatal(err)
}

// first init with old version
stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir="+cachePath, "-no-color")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}

firstExpectedMsgs := []string{
"Installed hashicorp/simple6 v1.2.3",
"Using previously-installed hashicorp/simple6 v1.2.3",
}
for _, msg := range firstExpectedMsgs {
if !strings.Contains(stdout, msg) {
t.Errorf("unexpected output from first init, expected %q, but got:\n%s", msg, stdout)
}
}

// build new version
newProviderTmp := filepath.Join(tf.WorkDir(), "terraform-provider-simple6")
newPath := filepath.Join(fsMirrorPath, "1.5.0", platform)
newProviderExe := e2e.GoBuild(
"github.com/hashicorp/terraform/internal/provider-simple-v6/main", newProviderTmp,
"-ldflags", "-X 'main.parentStateDir="+newStateDirName+"'")
if err := os.MkdirAll(newPath, os.ModePerm); err != nil {
t.Fatal(err)
}
targetPath = filepath.Join(newPath, "terraform-provider-simple6")
if err := os.Rename(newProviderExe, targetPath); err != nil {
t.Fatal(err)
}

backendStateFile := filepath.Join(tf.WorkDir(), ".terraform", "terraform.tfstate")
_, err = os.ReadFile(backendStateFile)
if err != nil {
t.Fatalf("failed to open backend state file: %s", err)
}

// second init with new version in place
stdout, stderr, err = tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir="+cachePath, "-no-color", "-upgrade", "-force-copy")
if err != nil {
t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr)
}

secondExpectedMsgs := []string{
"Installed hashicorp/simple6 v1.5.0",
"Using previously-installed hashicorp/simple6 v1.5.0",
}
for _, msg := range secondExpectedMsgs {
if !strings.Contains(stdout, msg) {
t.Errorf("unexpected output from second init, expected %q, but got:\n%s", msg, stdout)
}
}

fi, err := os.Stat(path.Join(tf.WorkDir(), newStateDirName, "terraform.tfstate.d", "default", "terraform.tfstate"))
if err != nil {
t.Fatalf("failed to open new default workspace's state file: %s", err)
}
if fi.Size() == 0 {
t.Fatal("default workspace's state file should not have size 0 bytes")
}

// TODO: check if both files match
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
terraform {
required_providers {
simple6 = {
source = "registry.terraform.io/hashicorp/simple6"
}
}

state_store "simple6_fs" {
provider "simple6" {}
}
}

variable "name" {
default = "world"
}

resource "terraform_data" "my-data" {
input = "hello ${var.name}"
}

output "greeting" {
value = resource.terraform_data.my-data.output
}
3 changes: 2 additions & 1 deletion internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra
return back, true, diags
}

func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, initArgs *arguments.Init, configLocks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, initArgs *arguments.Init, previousLocks *depsfile.Locks, configLocks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "initialize backend")
_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
defer span.End()
Expand Down Expand Up @@ -234,6 +234,7 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ini
opts = &BackendOpts{
StateStoreConfig: root.StateStore,
Locks: configLocks,
PreviousLocks: previousLocks,
ConfigOverride: configOverride,
Init: true,
ViewType: initArgs.ViewType,
Expand Down
4 changes: 2 additions & 2 deletions internal/command/init_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,10 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
case initArgs.Cloud && rootModEarly.CloudConfig != nil:
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
case initArgs.Backend:
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs, configLocks, view)
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs, previousLocks, configLocks, view)
default:
// load the previously-stored backend config
back, backDiags = c.Meta.backendFromState(ctx)
back, backDiags = c.Meta.backendFromState(ctx, previousLocks)
}
if backendOutput {
header = true
Expand Down
65 changes: 43 additions & 22 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ type BackendOpts struct {
// version in use currently.
Locks *depsfile.Locks

// PreviousLocks allows state-migration logic to use the older version of the provider
// during upgrade-triggered migrations.
PreviousLocks *depsfile.Locks

SkipGetProviderSchemaCache bool

// 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.
Expand Down Expand Up @@ -370,7 +376,7 @@ func (m *Meta) BackendForLocalPlan(plan *plans.Plan) (backendrun.OperationsBacke
return nil, diags
}

factories, err := m.ProviderFactoriesFromLocks(locks)
factories, err := m.ProviderFactoriesFromLocks(locks, false)
if err != nil {
// This may happen if the provider isn't present in the provider cache.
// This should be caught earlier by logic that diffs the config against the backend state file.
Expand Down Expand Up @@ -1044,7 +1050,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
v := views.NewInit(opts.ViewType, m.View)
v.Output(views.InitMessageCode("state_store_unset"), s.StateStore.Type)

return m.stateStore_to_backend(sMgr, "local", localB, nil, opts.ViewType)
return m.stateStore_to_backend(sMgr, "local", localB, nil, opts)

// Configuring a backend for the first time or -reconfigure flag was used
case backendConfig != nil && s.Backend.Empty() &&
Expand Down Expand Up @@ -1118,7 +1124,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
newBackendCfgState.SetConfig(configVal, b.ConfigSchema())
newBackendCfgState.Hash = uint64(cHash)

return m.stateStore_to_backend(sMgr, backendConfig.Type, b, newBackendCfgState, opts.ViewType)
return m.stateStore_to_backend(sMgr, backendConfig.Type, b, newBackendCfgState, opts)

// Migration from backend to state store
case backendConfig == nil && !s.Backend.Empty() &&
Expand Down Expand Up @@ -1220,7 +1226,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
// 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.StateStore.Hash) && (!opts.Init || opts.ConfigOverride == nil) {
log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q state_store configuration", stateStoreConfig.Type)
savedStateStore, sssDiags := m.savedStateStore(sMgr)
savedStateStore, sssDiags := m.savedStateStore(sMgr, opts.Locks, false)
diags = diags.Append(sssDiags)
// Verify that selected workspace exist. Otherwise prompt user to create one
if opts.Init && savedStateStore != nil {
Expand All @@ -1238,7 +1244,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
// don't need to migrate, we update the state store cache hash value.
if !m.stateStoreConfigNeedsMigration(stateStoreConfig, s.StateStore, opts) {
log.Printf("[TRACE] Meta.Backend: using already-initialized %q state store configuration", stateStoreConfig.Type)
savedStateStore, moreDiags := m.savedStateStore(sMgr)
savedStateStore, moreDiags := m.savedStateStore(sMgr, opts.Locks, false)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
Expand Down Expand Up @@ -1305,6 +1311,20 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
return nil, diags
}

pLock := opts.Locks.Provider(stateStoreConfig.ProviderAddr)
lockVersion, err := providerreqs.GoVersionFromVersion(pLock.Version())
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Unable to determine version of the state store provider",
fmt.Sprintf("Failed to parse version of %s from the lock file: %s", stateStoreConfig.ProviderAddr.ForDisplay(), err),
))
return nil, diags
}
if !lockVersion.Equal(s.StateStore.Provider.Version) {
opts.SkipGetProviderSchemaCache = true
}

return m.stateStore_changed(stateStoreConfig, cHash, sMgr, opts, initReason)

default:
Expand Down Expand Up @@ -1467,7 +1487,7 @@ func (m *Meta) determineStateStoreInitReason(cfgState *workdir.StateStoreConfigS
// from the backend state. This should be used only when a user runs
// `terraform init -backend=false`. This function returns a local backend if
// there is no backend state or no backend configured.
func (m *Meta) backendFromState(_ context.Context) (backend.Backend, tfdiags.Diagnostics) {
func (m *Meta) backendFromState(_ context.Context, locks *depsfile.Locks) (backend.Backend, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// Get the path to where we store a local cache of backend configuration
// if we're using a remote backend. This may not yet exist which means
Expand All @@ -1493,7 +1513,7 @@ func (m *Meta) backendFromState(_ context.Context) (backend.Backend, tfdiags.Dia
// state_store
log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q state store", s.StateStore.Type)
var ssDiags tfdiags.Diagnostics
b, ssDiags = m.savedStateStore(sMgr) // Relies on the state manager's internal state being refreshed above.
b, ssDiags = m.savedStateStore(sMgr, locks, false) // Relies on the state manager's internal state being refreshed above.
diags = diags.Append(ssDiags)
if ssDiags.HasErrors() {
return nil, diags
Expand Down Expand Up @@ -2531,17 +2551,17 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backend
}

// Migrating a state store to backend (including local).
func (m *Meta) stateStore_to_backend(ssSMgr *clistate.LocalState, dstBackendType string, dstBackend backend.Backend, newBackendState *workdir.BackendConfigState, viewType arguments.ViewType) (backend.Backend, tfdiags.Diagnostics) {
func (m *Meta) stateStore_to_backend(ssSMgr *clistate.LocalState, dstBackendType string, dstBackend backend.Backend, newBackendState *workdir.BackendConfigState, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

s := ssSMgr.State()
stateStoreType := s.StateStore.Type

view := views.NewInit(viewType, m.View)
view := views.NewInit(opts.ViewType, m.View)
view.Output(views.StateMigrateLocalMessage, stateStoreType)

// Initialize the configured state store
ss, moreDiags := m.savedStateStore(ssSMgr)
ss, moreDiags := m.savedStateStore(ssSMgr, opts.Locks, false)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
Expand All @@ -2553,7 +2573,7 @@ func (m *Meta) stateStore_to_backend(ssSMgr *clistate.LocalState, dstBackendType
DestinationType: dstBackendType,
Source: ss,
Destination: dstBackend,
ViewType: viewType,
ViewType: opts.ViewType,
})
if err != nil {
diags = diags.Append(err)
Expand Down Expand Up @@ -2668,7 +2688,7 @@ func (m *Meta) stateStore_changed(cfg *configs.StateStore, cfgHash int, sMgr *cl
}

// Grab the source state store
srcB, srcBDiags := m.savedStateStore(sMgr)
srcB, srcBDiags := m.savedStateStore(sMgr, opts.PreviousLocks, opts.SkipGetProviderSchemaCache)
diags = diags.Append(srcBDiags)
if srcBDiags.HasErrors() {
return nil, diags
Expand All @@ -2685,11 +2705,12 @@ func (m *Meta) stateStore_changed(cfg *configs.StateStore, cfgHash int, sMgr *cl

// Perform the migration
err := m.backendMigrateState(&backendMigrateOpts{
SourceType: s.StateStore.Type,
DestinationType: cfg.Type,
Source: srcB,
Destination: dstB,
ViewType: vt,
SkipGetProviderSchemaCache: opts.SkipGetProviderSchemaCache,
SourceType: s.StateStore.Type,
DestinationType: cfg.Type,
Source: srcB,
Destination: dstB,
ViewType: vt,
})
if err != nil {
diags = diags.Append(err)
Expand Down Expand Up @@ -2822,7 +2843,7 @@ To make the initial dependency selections that will initialize the dependency lo
}

// Initializing a saved state store from the backend state file (aka 'cache file', aka 'legacy state file')
func (m *Meta) savedStateStore(sMgr *clistate.LocalState) (backend.Backend, tfdiags.Diagnostics) {
func (m *Meta) savedStateStore(sMgr *clistate.LocalState, locks *depsfile.Locks, skipCache bool) (backend.Backend, tfdiags.Diagnostics) {
// We're preparing a state_store version of backend.Backend.
//
// The provider and state store will be configured using the backend state file.
Expand All @@ -2831,7 +2852,7 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState) (backend.Backend, tfdi

s := sMgr.State()

factory, pDiags := m.StateStoreProviderFactoryFromConfigState(s.StateStore)
factory, pDiags := m.StateStoreProviderFactoryFromConfigState(s.StateStore, locks, skipCache)
diags = diags.Append(pDiags)
if pDiags.HasErrors() {
return nil, diags
Expand Down Expand Up @@ -3313,7 +3334,7 @@ func (m *Meta) StateStoreProviderFactoryFromConfig(config *configs.StateStore, l
})
}

factories, err := m.ProviderFactoriesFromLocks(locks)
factories, err := m.ProviderFactoriesFromLocks(locks, false)
if err != nil {
// This may happen if the provider isn't present in the provider cache.
// This should be caught earlier by logic that diffs the config against the backend state file.
Expand Down Expand Up @@ -3346,7 +3367,7 @@ func (m *Meta) StateStoreProviderFactoryFromConfig(config *configs.StateStore, l
return factory, diags
}

func (m *Meta) StateStoreProviderFactoryFromConfigState(cfgState *workdir.StateStoreConfigState) (providers.Factory, tfdiags.Diagnostics) {
func (m *Meta) StateStoreProviderFactoryFromConfigState(cfgState *workdir.StateStoreConfigState, locks *depsfile.Locks, skipCache bool) (providers.Factory, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

if cfgState == nil {
Expand All @@ -3362,7 +3383,7 @@ func (m *Meta) StateStoreProviderFactoryFromConfigState(cfgState *workdir.StateS
})
}

factories, err := m.ProviderFactories()
factories, err := m.providerFactoriesFromLocks(locks, skipCache)
if err != nil {
// This may happen if the provider isn't present in the provider cache.
// This should be caught earlier by logic that diffs the config against the backend state file.
Expand Down
Loading
Loading