This commit is contained in:
Radek Simko 2026-02-03 18:04:31 +00:00 committed by GitHub
commit fc2b019ad8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 160 additions and 19 deletions

View file

@ -438,6 +438,7 @@ func (c *InitCommand) initPssBackend(ctx context.Context, root *configs.Module,
opts = &BackendOpts{
StateStoreConfig: root.StateStore,
ProviderRequirements: root.ProviderRequirements,
Locks: configLocks,
CreateDefaultWorkspace: initArgs.CreateDefaultWorkspace,
ConfigOverride: configOverride,

View file

@ -4630,6 +4630,47 @@ func TestInit_stateStore_to_backend(t *testing.T) {
}
}
func TestInit_unitialized_stateStore(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
cfg := `terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
state_store "test_store" {
provider "test" {}
value = "foobar"
}
}
`
if err := os.WriteFile(filepath.Join(td, "main.tf"), []byte(cfg), 0644); err != nil {
t.Fatalf("err: %s", err)
}
t.Chdir(td)
ui := cli.NewMockUi()
view, done := testView(t)
cApply := &ApplyCommand{
Meta: Meta{
Ui: ui,
View: view,
AllowExperimentalFeatures: true,
},
}
code := cApply.Run([]string{})
testOutput := done(t)
if code == 0 {
t.Fatalf("expected apply to fail: \n%s", testOutput.All())
}
log.Printf("[TRACE] TestInit_stateStore_to_backend: uninitialised apply with state store complete")
expectedErr := `provider registry.terraform.io/hashicorp/test: required by this configuration but no version is selected`
if !strings.Contains(testOutput.Stderr(), expectedErr) {
t.Fatalf("unexpected error, expected %q, given: %s", expectedErr, testOutput.Stderr())
}
}
// newMockProviderSource is a helper to succinctly construct a mock provider
// source that contains a set of packages matching the given provider versions
// that are available for installation (from temporary local files).

View file

@ -61,6 +61,8 @@ type BackendOpts struct {
// the root module, or nil if no such block is present.
StateStoreConfig *configs.StateStore
ProviderRequirements *configs.RequiredProviders
// Locks allows state-migration logic to detect when the provider used for pluggable state storage
// during the last init (i.e. what's in the backend state file) is mismatched with the provider
// version in use currently.
@ -799,6 +801,33 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, tf
return nil, 0, diags
}
if errs := c.VerifyDependencySelections(opts.Locks, opts.ProviderRequirements); len(errs) > 0 {
var buf strings.Builder
for _, err := range errs {
fmt.Fprintf(&buf, "\n - %s", err.Error())
}
var suggestion string
switch {
case opts.Locks == nil:
// If we get here then it suggests that there's a caller that we
// didn't yet update to populate DependencyLocks, which is a bug.
panic("This run has no dependency lock information provided at all, which is a bug in Terraform; please report it!")
case opts.Locks.Empty():
suggestion = "To make the initial dependency selections that will initialize the dependency lock file, run:\n terraform init"
default:
suggestion = "To update the locked dependency selections to match a changed configuration, run:\n terraform init -upgrade"
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
fmt.Sprintf(
"The following dependency selections recorded in the lock file are inconsistent with the current configuration:%s\n\n%s",
buf.String(), suggestion,
),
))
return nil, 0, diags
}
// Get the provider version from locks, as this impacts the hash
// NOTE: this assumes that we will never allow users to override config definint which provider is used for state storage
stateStoreProviderVersion, vDiags := getStateStorageProviderVersion(opts.StateStoreConfig, opts.Locks)
@ -1902,9 +1931,10 @@ func (m *Meta) backend(configPath string, viewType arguments.ViewType) (backendr
}
case root.StateStore != nil:
opts = &BackendOpts{
StateStoreConfig: root.StateStore,
Locks: locks,
ViewType: viewType,
StateStoreConfig: root.StateStore,
ProviderRequirements: root.ProviderRequirements,
Locks: locks,
ViewType: viewType,
}
default:
// there is no config; defaults to local state storage
@ -2230,6 +2260,8 @@ func getStateStorageProviderVersion(c *configs.StateStore, locks *depsfile.Locks
pLock := locks.Provider(c.ProviderAddr)
if pLock == nil {
// This should never happen as the user would've already hit
// an error earlier prompting them to run init
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.",
c.Provider.Name,
c.ProviderAddr,

View file

@ -2298,9 +2298,10 @@ func TestMetaBackend_configuredBackendToStateStore(t *testing.T) {
[]providerreqs.Hash{""},
)
_, beDiags := m.Backend(&BackendOpts{
Init: true,
StateStoreConfig: mod.StateStore,
Locks: locks,
Init: true,
StateStoreConfig: mod.StateStore,
ProviderRequirements: mod.ProviderRequirements,
Locks: locks,
})
if !beDiags.HasErrors() {
t.Fatal("expected an error to be returned during partial implementation of PSS")
@ -2364,9 +2365,10 @@ func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) {
// Get the operations backend
_, err := m.Backend(&BackendOpts{
Init: true,
StateStoreConfig: mod.StateStore,
Locks: locks,
Init: true,
StateStoreConfig: mod.StateStore,
ProviderRequirements: mod.ProviderRequirements,
Locks: locks,
})
if err == nil {
t.Fatal("should error")
@ -2823,10 +2825,11 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) {
overrideValue := "overridden"
configOverride := configs.SynthBody("synth", map[string]cty.Value{"value": cty.StringVal(overrideValue)})
opts := &BackendOpts{
StateStoreConfig: config,
ConfigOverride: configOverride,
Init: true,
Locks: locks,
StateStoreConfig: config,
ProviderRequirements: &configs.RequiredProviders{},
ConfigOverride: configOverride,
Init: true,
Locks: locks,
}
mock := testStateStoreMock(t)
@ -2882,9 +2885,10 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) {
delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Remove the only state store impl.
opts := &BackendOpts{
StateStoreConfig: config,
Init: true,
Locks: locks,
StateStoreConfig: config,
ProviderRequirements: &configs.RequiredProviders{},
Init: true,
Locks: locks,
}
m := testMetaBackend(t, nil)
@ -2910,9 +2914,10 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) {
mock.GetProviderSchemaResponse.StateStores["test_bore"] = testStore
opts := &BackendOpts{
StateStoreConfig: config,
Init: true,
Locks: locks,
StateStoreConfig: config,
ProviderRequirements: &configs.RequiredProviders{},
Init: true,
Locks: locks,
}
m := testMetaBackend(t, nil)

View file

@ -5,7 +5,9 @@ package configs
import (
"fmt"
"log"
"os"
"sort"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
@ -13,6 +15,8 @@ import (
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/depsfile"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/getproviders/reattach"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
@ -133,6 +137,64 @@ func resolveStateStoreProviderType(requiredProviders map[string]*RequiredProvide
}
}
func (ss *StateStore) VerifyDependencySelections(depLocks *depsfile.Locks, reqs *RequiredProviders) []error {
var errs []error
for _, reqProvider := range reqs.RequiredProviders {
providerAddr := reqProvider.Type
constraints := providerreqs.MustParseVersionConstraints(reqProvider.Requirement.Required.String())
if !depsfile.ProviderIsLockable(providerAddr) {
continue // disregard builtin providers, and such
}
if depLocks != nil && depLocks.ProviderIsOverridden(providerAddr) {
// The "overridden" case is for unusual special situations like
// dev overrides, so we'll explicitly note it in the logs just in
// case we see bug reports with these active and it helps us
// understand why we ended up using the "wrong" plugin.
log.Printf("[DEBUG] StateStore.VerifyDependencySelections: skipping %s because it's overridden by a special configuration setting", providerAddr)
continue
}
var lock *depsfile.ProviderLock
if depLocks != nil { // Should always be true in main code, but unfortunately sometimes not true in old tests that don't fill out arguments completely
lock = depLocks.Provider(providerAddr)
}
if lock == nil {
log.Printf("[TRACE] StateStore.VerifyDependencySelections: provider %s has no lock file entry to satisfy %q", providerAddr, providerreqs.VersionConstraintsString(constraints))
errs = append(errs, fmt.Errorf("provider %s: required by this configuration but no version is selected", providerAddr))
continue
}
selectedVersion := lock.Version()
allowedVersions := providerreqs.MeetingConstraints(constraints)
log.Printf("[TRACE] StateStore.VerifyDependencySelections: provider %s has %s to satisfy %q", providerAddr, selectedVersion.String(), providerreqs.VersionConstraintsString(constraints))
if !allowedVersions.Has(selectedVersion) {
// The most likely cause of this is that the author of a module
// has changed its constraints, but this could also happen in
// some other unusual situations, such as the user directly
// editing the lock file to record something invalid. We'll
// distinguish those cases here in order to avoid the more
// specific error message potentially being a red herring in
// the edge-cases.
currentConstraints := providerreqs.VersionConstraintsString(constraints)
lockedConstraints := providerreqs.VersionConstraintsString(lock.VersionConstraints())
switch {
case currentConstraints != lockedConstraints:
errs = append(errs, fmt.Errorf("provider %s: locked version selection %s doesn't match the updated version constraints %q", providerAddr, selectedVersion.String(), currentConstraints))
default:
errs = append(errs, fmt.Errorf("provider %s: version constraints %q don't match the locked version selection %s", providerAddr, currentConstraints, selectedVersion.String()))
}
}
}
// Return multiple errors in an arbitrary-but-deterministic order.
sort.Slice(errs, func(i, j int) bool {
return errs[i].Error() < errs[j].Error()
})
return errs
}
// Hash produces a hash value for the receiver that covers:
// 1) the portions of the config that conform to the state_store schema.
// 2) the portions of the config that conform to the provider schema.