You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/configs/state_store.go

369 lines
16 KiB

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package configs
import (
"fmt"
"log"
version "github.com/hashicorp/go-version"
"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/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// StateStore represents a "state_store" block inside a "terraform" block
// in a module or file.
type StateStore struct {
// Type is a state store type name
Type string
// Config is the full configuration of the state_store block, including the
// nested provider block. The nested provider block config is accessible
// in isolation via (StateStore).Provider.Config
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
// ProviderSupplyMode describes how the provider used for state storage was supplied to Terraform.
// This is needed when handling provider version data; unmanaged and builtin providers have no version data available.
// This value is ultimately recorded in the backend state file alongside the provider version data (which may be nil).
ProviderSupplyMode getproviders.ProviderSupplyMode
TypeRange hcl.Range
DeclRange hcl.Range
}
func decodeStateStoreBlock(block *hcl.Block) (*StateStore, hcl.Diagnostics) {
var diags hcl.Diagnostics
ss := &StateStore{
Type: block.Labels[0],
TypeRange: block.LabelRanges[0],
Config: block.Body,
DeclRange: block.DefRange,
}
content, remain, moreDiags := block.Body.PartialContent(StateStorageBlockSchema)
diags = append(diags, moreDiags...)
ss.Config = remain
if len(content.Blocks) == 0 {
return nil, append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing provider block",
Detail: "A 'provider' block is required in 'state_store' blocks",
Subject: block.Body.MissingItemRange().Ptr(),
})
}
if len(content.Blocks) > 1 {
return nil, append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate provider block",
Detail: "Only one 'provider' block should be present in a 'state_store' block",
Subject: &content.Blocks[1].DefRange,
})
}
providerBlock := content.Blocks[0]
provider, providerDiags := decodeProviderBlock(providerBlock, false)
if providerDiags.HasErrors() {
return nil, append(diags, providerDiags...)
}
if provider.AliasRange != nil {
// This block is in its own namespace in the state_store block; aliases are irrelevant
return nil, append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unexpected provider alias",
Detail: "Aliases are disallowed in the 'provider' block in the 'state_store' block",
Subject: provider.AliasRange,
})
}
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
}
var StateStorageBlockSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "provider",
LabelNames: []string{"type"},
},
},
}
// 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 = diags.Append(
&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 %s",
stateStore.Provider.Name,
),
Subject: &stateStore.DeclRange,
},
)
return tfaddr.Provider{}, diags
default:
// We've got a required_providers entry to use
// This code path is used for both re-attached providers
// providers that are fully managed by Terraform.
return addr.Type, nil
}
}
// VerifyDependencySelection checks whether the provider used for state storage has a valid version in the
// dependency lock file that matches the constraints in required_providers.
// There is also special handling for providers that cannot be represented in the lock file (built-in providers, dev overrides)
// and also special handling when the provider is re-attached and not managed by Terraform.
func (ss *StateStore) VerifyDependencySelection(depLocks *depsfile.Locks, reqs *RequiredProviders, supplyMode getproviders.ProviderSupplyMode) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// If we get nil arguments it suggests that there's a bug in the calling code.
if depLocks == nil {
panic("This run has no dependency lock information provided at all. This is a bug in Terraform and should be reported.")
}
if reqs == nil {
panic("This run has no required providers information provided at all. This is a bug in Terraform and should be reported.")
}
if supplyMode.NotManagedByTerraform() {
// If the provider is not managed by Terraform then it's not lockable.
// If the working directory was initialized in the same way then the PSS provider will not be reflected in the lock file.
// Skip them.
switch supplyMode {
case getproviders.BuiltIn:
log.Printf("[DEBUG] StateStore.VerifyDependencySelection: skipping %s because it's a built-in provider", ss.ProviderAddr)
case getproviders.DevOverride:
log.Printf("[DEBUG] StateStore.VerifyDependencySelection: skipping %s because it's supplied via developer overrides", ss.ProviderAddr)
case getproviders.Reattached:
log.Printf("[DEBUG] StateStore.VerifyDependencySelection: skipping %s because it's re-attached and not managed by Terraform", ss.ProviderAddr)
default:
panic(fmt.Sprintf("State store provider %q (%s) has unknown supply mode %q. This is a bug in Terraform and should be reported.", ss.ProviderAddr.Type, ss.ProviderAddr.ForDisplay(), supplyMode))
}
return diags
}
// From this point on the provider currently in use is managed by Terraform
//
// When the PSS provider is managed, Terraform needs the state storage provider to be present in the lock file,
// and the lock file should not be empty or missing.
if depLocks.Empty() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
fmt.Sprintf(`The provider dependency used for state storage is missing from the lock file despite being present in the current configuration:
- provider %s: required by this configuration but no version is selected
To make the initial dependency selections that will initialize the dependency lock file, run:
terraform init`,
ss.ProviderAddr,
),
))
return diags
}
req, ok := reqs.RequiredProviders[ss.ProviderAddr.Type]
if !ok {
// The provider used for state storage is not in the required providers list.
// This should have been identified when the block was parsed, so if we get here
// it suggests that upstream code is swallowing that error.
panic("State store provider is missing from required providers but this was not caught during config parsing, which is a bug in Terraform; please report it!")
}
// Is the provider in the lock file, and is it an appropriate version matching the constraints in required_providers?
lock := depLocks.Provider(ss.ProviderAddr)
constraints := providerreqs.MustParseVersionConstraints(req.Requirement.Required.String())
if lock == nil {
log.Printf("[TRACE] StateStore.VerifyDependencySelections: provider %s has no lock file entry to satisfy %q", ss.ProviderAddr, providerreqs.VersionConstraintsString(constraints))
return diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
fmt.Sprintf(`The provider dependency used for state storage recorded in the lock file is inconsistent with the current configuration:
- provider %s: required by this configuration but no version is selected
To make the initial dependency selections that will initialize the dependency lock file, run:
terraform init`,
ss.ProviderAddr,
),
))
}
selectedVersion := lock.Version()
allowedVersions := providerreqs.MeetingConstraints(constraints)
log.Printf("[TRACE] StateStore.VerifyDependencySelection: provider %s has %s to satisfy %q", ss.ProviderAddr, selectedVersion.String(), providerreqs.VersionConstraintsString(constraints))
if !allowedVersions.Has(selectedVersion) {
currentConstraints := providerreqs.VersionConstraintsString(constraints)
lockedConstraints := providerreqs.VersionConstraintsString(lock.VersionConstraints())
switch {
case currentConstraints != lockedConstraints:
return diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
fmt.Sprintf(`The provider dependency used for state storage recorded in the lock file is inconsistent with the current configuration:
- provider %s: locked version selection %s doesn't match the updated version constraints %q
To update the locked dependency selections to match a changed configuration, run:
terraform init -upgrade`,
ss.ProviderAddr,
selectedVersion.String(),
currentConstraints,
),
))
default:
return diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Inconsistent dependency lock file",
fmt.Sprintf(`The provider dependency used for state storage recorded in the lock file is inconsistent with the current configuration:
- provider %s: version constraints %q don't match the locked version selection %s
To make the initial dependency selections that will initialize the dependency lock file, run:
terraform init`,
ss.ProviderAddr,
selectedVersion.String(),
currentConstraints,
),
))
}
}
return diags
}
// 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.
// 3) the state store type
// 4) the provider source
// 5) the provider name
// 6) the provider version
//
// If the config does not conform to the schema then the result is not
// meaningful for comparison since it will be based on an incomplete result.
//
// As an exception, required attributes in the schema are treated as optional
// for the purpose of hashing, so that an incomplete configuration can still
// be hashed. Other errors, such as extraneous attributes, have no such special
// case.
func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema *configschema.Block, stateStoreProviderVersion *version.Version) (int, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// 1. Prepare the state_store hash
// The state store schema should not include a provider block or attr
if _, exists := stateStoreSchema.Attributes["provider"]; exists {
return 0, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Protected argument name \"provider\" in state store schema",
Detail: "Schemas for state stores cannot contain attributes or blocks called \"provider\", to avoid confusion with the provider block nested inside the state_store block. This is a bug in the provider used for state storage, which should be reported in the provider's own issue tracker.",
Context: &b.Provider.DeclRange,
})
}
if _, exists := stateStoreSchema.BlockTypes["provider"]; exists {
return 0, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Protected block name \"provider\" in state store schema",
Detail: "Schemas for state stores cannot contain attributes or blocks called \"provider\", to avoid confusion with the provider block nested inside the state_store block. This is a bug in the provider used for state storage, which should be reported in the provider's own issue tracker.",
Context: &b.Provider.DeclRange,
})
}
// Don't fail if required attributes are not set. Instead, we'll just
// hash them as nulls.
schema := stateStoreSchema.NoneRequired()
spec := schema.DecoderSpec()
// The value `b.Config` will include data about the provider block nested inside state_store
// so we need to ignore it. Decode will return errors about 'extra' attrs and blocks. We can ignore
// the diagnostic reporting the unexpected provider block, but we need to handle all other diagnostics.
// but we need to check that's the only thing being ignored.
ssVal, decodeDiags := hcldec.Decode(b.Config, spec, nil)
if decodeDiags.HasErrors() {
for _, diag := range decodeDiags {
diags = diags.Append(diag)
}
if diags.HasErrors() {
return 0, diags
}
}
// We're on the happy path, but handle if we got a nil value above
if ssVal == cty.NilVal {
ssVal = cty.UnknownVal(schema.ImpliedType())
}
// 2. Prepare the provider hash
schema = providerSchema.NoneRequired()
spec = schema.DecoderSpec()
pVal, decodeDiags := hcldec.Decode(b.Provider.Config, spec, nil)
if decodeDiags.HasErrors() {
diags = diags.Append(decodeDiags)
return 0, diags
}
if pVal == cty.NilVal {
pVal = cty.UnknownVal(schema.ImpliedType())
}
var providerVersionString string
switch b.ProviderSupplyMode {
case getproviders.BuiltIn, getproviders.DevOverride, getproviders.Reattached:
// We expect to not have version information in these situations.
// We'll use an empty string for the hash.
providerVersionString = ""
case getproviders.ManagedByTerraform:
if stateStoreProviderVersion == nil {
// Lack of version information indicates a problem; error
return 0, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to calculate hash of state store configuration",
Detail: "Provider version data was missing during hash generation. This is a bug in Terraform and should be reported.",
})
}
providerVersionString = stateStoreProviderVersion.String()
default:
panic(fmt.Sprintf("State store provider %q (%s) has unknown supply mode %q. This is a bug in Terraform and should be reported.", b.ProviderAddr.Type, b.ProviderAddr.ForDisplay(), b.ProviderSupplyMode))
}
toHash := cty.TupleVal([]cty.Value{
cty.StringVal(b.Type), // state store type
ssVal, // state store config
cty.StringVal(b.ProviderAddr.String()), // provider source
cty.StringVal(providerVersionString), // provider version
cty.StringVal(b.Provider.Name), // provider name - this is directly parsed from the config, whereas provider source is added separately later after config is parsed.
pVal, // provider config
})
return toHash.Hash(), diags
}