Skip to content

Commit efb2a1c

Browse files
authored
PSS: Let the init command recognise when there are no changes in configuration. (#37777)
* Pull determining of PSS provider's version from current locks into a separate method * Add code for identifying when config and provider version match existing backend state (i.e. no changes) * Update test - locks are now needed before it hits expected error diag return * Add test showing successful init when no config changes are detected. * Update `getStateStorageProviderVersion` to return nil versions for builtin and re-attached providers. This makes comparison easier when determining if config has changed since last init. * Add test coverage for `getStateStorageProviderVersion` * Move testing fixtures around, preparing for different types of changed state_store config changes being tested * Add test showing that changing the state_store config is detected as a change, but handling this scenario isn't implemented yet * Update hashes in test fixture backend state file to be accurate Previously dummy values were fine, but as tests using hashes to identify changes these values need to be accurate! * Update existing test cases so that Terraform uses the same test provider version as described in the backend state file fixture for the test. * Add test showing that changing the PSS provider's config is detected as a change, but handling this scenario isn't implemented yet * Add test showing that swapping to a different state storage implementation in the same provider is detected as a change, but handling this scenario isn't implemented yet * Add test showing that changing the provider used for PSS is detected as a change, but handling this scenario isn't implemented yet * Add test showing that upgrading a provider is detected as a change, but handling this scenario isn't implemented yet * Update test to use v1.2.3 for consistency with other tests Just to avoid any confusion if copy-pasting happens in future. * More corrections to existing test fixtures - unset config should be null, and replace dummy hash values with correct values. * Fix test for using -reconfigure with state_store; the default workspace would already exist in this scenario * Update TestInit_stateStore_configUnchanged to assert that init was a no-op for backend state * Remove unused fixture * Remove test that's replaced by new tests in command/init_test.go * Replace old references to deleted "state-store-changed" test fixture & update test to not expect a value for region attr in provider config * Make test fixture coupling a little more understandable * Refactor detection of no need to migrate into a function * Add TODO about more involved provider version change tests We will allow downgrades to succeed as long as the schema version number is unchanged * Update (configs.StateStore)Hash method to return a single hash that's impacted by: state store config, provider config, state store type, provider source * Update calling code and test helper code to reflect that the nested provider block no longer has its own hash * Remove test; there is now a single hash that SHOULD be affected by the provider block! * Also use provider name, from config, in hash * Update tests to reflect changes in how hashes are made * Remove unused `stateStoreConfigNeedsMigration` function * Remove duplicate isProviderReattached function. * Fixes to affected tests * Allow provider version to impact the state storage hash, update impacted tests and test fixtures * Update tests that now require locks data to be present in test setup * Update comment for accuracy * Fixes to other test fixtures - remove excess hash field, set hash to 0 to indicate they're not set accurately. * Make upgrade test actually use upgrade code path * Add lock files to test fixture directories that represent a project that's had a successful prior init using PSS
1 parent 106f8c6 commit efb2a1c

File tree

28 files changed

+1032
-292
lines changed

28 files changed

+1032
-292
lines changed

internal/command/init_test.go

Lines changed: 384 additions & 22 deletions
Large diffs are not rendered by default.

internal/command/meta_backend.go

Lines changed: 92 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags.
568568
// > Ensures that that state store type exists in the linked provider.
569569
// > Returns config that is the combination of config and any config overrides originally supplied via the CLI.
570570
// > Returns a hash of the config in the configuration files, i.e. excluding overrides
571-
func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, int, tfdiags.Diagnostics) {
571+
func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, tfdiags.Diagnostics) {
572572
var diags tfdiags.Diagnostics
573573

574574
c := opts.StateStoreConfig
@@ -580,7 +580,15 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in
580580
Summary: "Missing state store configuration",
581581
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.",
582582
})
583-
return nil, 0, 0, diags
583+
return nil, 0, diags
584+
}
585+
586+
// Get the provider version from locks, as this impacts the hash
587+
// NOTE: this assumes that we will never allow users to override config definint which provider is used for state storage
588+
stateStoreProviderVersion, vDiags := getStateStorageProviderVersion(opts.StateStoreConfig, opts.Locks)
589+
diags = diags.Append(vDiags)
590+
if vDiags.HasErrors() {
591+
return nil, 0, diags
584592
}
585593

586594
// Check - is the state store type in the config supported by the provider?
@@ -590,12 +598,12 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in
590598
Summary: "Missing provider details when configuring state store",
591599
Detail: "Terraform attempted to configure a state store and no provider factory was available to launch it. This is a bug in Terraform and should be reported.",
592600
})
593-
return nil, 0, 0, diags
601+
return nil, 0, diags
594602
}
595603
provider, err := opts.ProviderFactory()
596604
if err != nil {
597605
diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
598-
return nil, 0, 0, diags
606+
return nil, 0, diags
599607
}
600608
defer provider.Close() // Stop the child process once we're done with it here.
601609

@@ -610,7 +618,7 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in
610618
c.ProviderAddr),
611619
Subject: &c.DeclRange,
612620
})
613-
return nil, 0, 0, diags
621+
return nil, 0, diags
614622
}
615623

616624
stateStoreSchema, exists := resp.StateStores[c.Type]
@@ -628,7 +636,7 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in
628636
c.ProviderAddr, suggestion),
629637
Subject: &c.DeclRange,
630638
})
631-
return nil, 0, 0, diags
639+
return nil, 0, diags
632640
}
633641

634642
// We know that the provider contains a state store with the correct type name.
@@ -638,21 +646,21 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in
638646
// > Apply any overrides
639647

640648
configBody := c.Config
641-
stateStoreHash, providerHash, diags := c.Hash(stateStoreSchema.Body, resp.Provider.Body)
649+
hash, diags := c.Hash(stateStoreSchema.Body, resp.Provider.Body, stateStoreProviderVersion)
642650

643651
// If we have an override configuration body then we must apply it now.
644652
if opts.ConfigOverride != nil {
645653
log.Println("[TRACE] Meta.Backend: merging -backend-config=... CLI overrides into state_store configuration")
646654
configBody = configs.MergeBodies(configBody, opts.ConfigOverride)
647655
}
648656

649-
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)
657+
log.Printf("[TRACE] Meta.Backend: built configuration for %q state_store with hash value %d", c.Type, hash)
650658

651659
// We'll shallow-copy configs.StateStore here so that we can replace the
652660
// body without affecting others that hold this reference.
653661
configCopy := *c
654662
configCopy.Config = configBody
655-
return &configCopy, stateStoreHash, providerHash, diags
663+
return &configCopy, hash, diags
656664
}
657665

658666
// backendFromConfig returns the initialized (not configured) backend
@@ -673,13 +681,11 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
673681
// Get the local 'backend' or 'state_store' configuration.
674682
var backendConfig *configs.Backend
675683
var stateStoreConfig *configs.StateStore
676-
var backendHash int
677-
var stateStoreHash int
678-
var stateStoreProviderHash int
684+
var cHash int
679685
if opts.StateStoreConfig != nil {
680686
// state store has been parsed from config and is included in opts
681687
var ssDiags tfdiags.Diagnostics
682-
stateStoreConfig, stateStoreHash, stateStoreProviderHash, ssDiags = m.stateStoreConfig(opts)
688+
stateStoreConfig, cHash, ssDiags = m.stateStoreConfig(opts)
683689
diags = diags.Append(ssDiags)
684690
if ssDiags.HasErrors() {
685691
return nil, diags
@@ -688,7 +694,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
688694
// backend config may or may not have been parsed and included in opts,
689695
// or may not exist in config at all (default/implied local backend)
690696
var beDiags tfdiags.Diagnostics
691-
backendConfig, backendHash, beDiags = m.backendConfig(opts)
697+
backendConfig, cHash, beDiags = m.backendConfig(opts)
692698
diags = diags.Append(beDiags)
693699
if beDiags.HasErrors() {
694700
return nil, diags
@@ -779,7 +785,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
779785
return nil, diags
780786
}
781787

782-
return m.backend_c_r_S(backendConfig, backendHash, sMgr, true, opts)
788+
return m.backend_c_r_S(backendConfig, cHash, sMgr, true, opts)
783789

784790
// We're unsetting a state_store (moving from state_store => local)
785791
case stateStoreConfig == nil && !s.StateStore.Empty() &&
@@ -809,7 +815,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
809815
}
810816
return nil, diags
811817
}
812-
return m.backend_C_r_s(backendConfig, backendHash, sMgr, opts)
818+
return m.backend_C_r_s(backendConfig, cHash, sMgr, opts)
813819

814820
// Configuring a state store for the first time or -reconfigure flag was used
815821
case stateStoreConfig != nil && s.StateStore.Empty() &&
@@ -830,7 +836,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
830836
return nil, diags
831837
}
832838

833-
return m.stateStore_C_s(stateStoreConfig, stateStoreHash, stateStoreProviderHash, sMgr, opts)
839+
return m.stateStore_C_s(stateStoreConfig, cHash, sMgr, opts)
834840

835841
// Migration from state store to backend
836842
case backendConfig != nil && s.Backend.Empty() &&
@@ -870,7 +876,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
870876
// We're not initializing
871877
// AND the backend cache hash values match, indicating that the stored config is valid and completely unchanged.
872878
// AND we're not providing any overrides. An override can mean a change overriding an unchanged backend block (indicated by the hash value).
873-
if (uint64(backendHash) == s.Backend.Hash) && (!opts.Init || opts.ConfigOverride == nil) {
879+
if (uint64(cHash) == s.Backend.Hash) && (!opts.Init || opts.ConfigOverride == nil) {
874880
log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q backend configuration", backendConfig.Type)
875881
savedBackend, diags := m.savedBackend(sMgr)
876882
// Verify that selected workspace exist. Otherwise prompt user to create one
@@ -898,7 +904,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
898904
// It's possible for a backend to be unchanged, and the config itself to
899905
// have changed by moving a parameter from the config to `-backend-config`
900906
// In this case, we update the Hash.
901-
moreDiags = m.updateSavedBackendHash(backendHash, sMgr)
907+
moreDiags = m.updateSavedBackendHash(cHash, sMgr)
902908
if moreDiags.HasErrors() {
903909
return nil, diags
904910
}
@@ -929,7 +935,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
929935
}
930936

931937
log.Printf("[WARN] backend config has changed since last init")
932-
return m.backend_C_r_S_changed(backendConfig, backendHash, sMgr, true, opts)
938+
return m.backend_C_r_S_changed(backendConfig, cHash, sMgr, true, opts)
933939

934940
// Potentially changing a state store configuration
935941
case backendConfig == nil && s.Backend.Empty() &&
@@ -944,6 +950,26 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
944950
// > Changing how the store is configured.
945951
// > Allowing values to be moved between partial overrides and config
946952

953+
// We're not initializing
954+
// AND the config's and backend state file's hash values match, indicating that the stored config is valid and completely unchanged.
955+
// AND we're not providing any overrides. An override can mean a change overriding an unchanged backend block (indicated by the hash value).
956+
if (uint64(cHash) == s.StateStore.Hash) && (!opts.Init || opts.ConfigOverride == nil) {
957+
log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q state_store configuration", stateStoreConfig.Type)
958+
savedStateStore, sssDiags := m.savedStateStore(sMgr, opts.ProviderFactory)
959+
diags = diags.Append(sssDiags)
960+
// Verify that selected workspace exist. Otherwise prompt user to create one
961+
if opts.Init && savedStateStore != nil {
962+
if err := m.selectWorkspace(savedStateStore); err != nil {
963+
diags = diags.Append(err)
964+
return nil, diags
965+
}
966+
}
967+
return savedStateStore, diags
968+
}
969+
970+
// Above caters only for unchanged config
971+
// but this switch case will also handle changes,
972+
// which isn't implemented yet.
947973
return nil, diags.Append(&hcl.Diagnostic{
948974
Severity: hcl.DiagError,
949975
Summary: "Not implemented yet",
@@ -1569,7 +1595,7 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
15691595
//-------------------------------------------------------------------
15701596

15711597
// Configuring a state_store for the first time.
1572-
func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, providerHash int, backendSMgr *clistate.LocalState, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) {
1598+
func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backendSMgr *clistate.LocalState, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) {
15731599
var diags tfdiags.Diagnostics
15741600

15751601
vt := arguments.ViewJSON
@@ -1691,33 +1717,21 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide
16911717
} else {
16921718
// The provider is not built in and is being managed by Terraform
16931719
// This is the most common scenario, by far.
1694-
pLock := opts.Locks.Provider(c.ProviderAddr)
1695-
if pLock == nil {
1696-
diags = diags.Append(fmt.Errorf("The provider %s (%q) is not present in the lockfile, despite being used for state store %q. This is a bug in Terraform and should be reported.",
1697-
c.Provider.Name,
1698-
c.ProviderAddr,
1699-
c.Type))
1700-
return nil, diags
1701-
}
1702-
var err error
1703-
pVersion, err = providerreqs.GoVersionFromVersion(pLock.Version())
1704-
if err != nil {
1705-
diags = diags.Append(fmt.Errorf("Failed obtain the in-use version of provider %s (%q) when recording backend state for state store %q. This is a bug in Terraform and should be reported: %w",
1706-
c.Provider.Name,
1707-
c.ProviderAddr,
1708-
c.Type,
1709-
err))
1720+
var vDiags tfdiags.Diagnostics
1721+
pVersion, vDiags = getStateStorageProviderVersion(c, opts.Locks)
1722+
diags = diags.Append(vDiags)
1723+
if vDiags.HasErrors() {
17101724
return nil, diags
17111725
}
17121726
}
17131727
}
1728+
17141729
s.StateStore = &workdir.StateStoreConfigState{
17151730
Type: c.Type,
17161731
Hash: uint64(stateStoreHash),
17171732
Provider: &workdir.ProviderConfigState{
17181733
Source: &c.ProviderAddr,
17191734
Version: pVersion,
1720-
Hash: uint64(providerHash),
17211735
},
17221736
}
17231737
s.StateStore.SetConfig(storeConfigVal, b.ConfigSchema())
@@ -1794,6 +1808,46 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide
17941808
return b, diags
17951809
}
17961810

1811+
// getStateStorageProviderVersion gets the current version of the state store provider that's in use. This is achieved
1812+
// by inspecting the current locks.
1813+
//
1814+
// This function assumes that calling code has checked whether the provider is fully managed by Terraform,
1815+
// or is built-in, before using this method and is prepared to receive a nil Version.
1816+
func getStateStorageProviderVersion(c *configs.StateStore, locks *depsfile.Locks) (*version.Version, tfdiags.Diagnostics) {
1817+
var diags tfdiags.Diagnostics
1818+
var pVersion *version.Version
1819+
1820+
isBuiltin := c.ProviderAddr.Hostname == addrs.BuiltInProviderHost
1821+
isReattached, err := reattach.IsProviderReattached(c.ProviderAddr, os.Getenv("TF_REATTACH_PROVIDERS"))
1822+
if err != nil {
1823+
diags = diags.Append(err)
1824+
return nil, diags
1825+
}
1826+
if isBuiltin || isReattached {
1827+
return nil, nil // nil Version returned
1828+
}
1829+
1830+
pLock := locks.Provider(c.ProviderAddr)
1831+
if pLock == nil {
1832+
diags = diags.Append(fmt.Errorf("The provider %s (%q) is not present in the lockfile, despite being used for state store %q. This is a bug in Terraform and should be reported.",
1833+
c.Provider.Name,
1834+
c.ProviderAddr,
1835+
c.Type))
1836+
return nil, diags
1837+
}
1838+
pVersion, err = providerreqs.GoVersionFromVersion(pLock.Version())
1839+
if err != nil {
1840+
diags = diags.Append(fmt.Errorf("Failed obtain the in-use version of provider %s (%q) used with state store %q. This is a bug in Terraform and should be reported: %w",
1841+
c.Provider.Name,
1842+
c.ProviderAddr,
1843+
c.Type,
1844+
err))
1845+
return nil, diags
1846+
}
1847+
1848+
return pVersion, diags
1849+
}
1850+
17971851
// createDefaultWorkspace receives a backend made using a pluggable state store, and details about that store's config,
17981852
// and persists an empty state file in the default workspace. By creating this artifact we ensure that the default
17991853
// workspace is created and usable by Terraform in later operations.

0 commit comments

Comments
 (0)