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_test.go

441 lines
14 KiB

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package configs
import (
"strings"
"testing"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
tfaddr "github.com/hashicorp/terraform-registry-address"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/zclconf/go-cty/cty"
)
// The Hash method assumes that the state_store schema doesn't include a provider block,
// and it requires calling code to remove the nested provider block from state_store config data.
func TestStateStore_Hash(t *testing.T) {
// Normally these schemas would come from a provider's GetProviderSchema data
exampleStateStoreSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {
Type: cty.String,
Required: true,
},
"workspace_dir": {
Type: cty.String,
Optional: true,
},
},
}
exampleProviderSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foobar": {
Type: cty.String,
Required: true,
},
},
}
// These values are all coupled.
// The test case below asserts that given these inputs, the expected hash is returned.
exampleProviderVersion := version.Must(version.NewSemver("1.2.3"))
exampleProviderAddr := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar")
exampleConfig := configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`)
exampleHash := 614398732
t.Run("example happy path with all attrs set in the configuration", func(t *testing.T) {
// Construct a configs.StateStore for the test.
content, _, cfgDiags := exampleConfig.PartialContent(terraformBlockSchema)
if len(cfgDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", cfgDiags)
}
var ssDiags hcl.Diagnostics
s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0])
if len(ssDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", ssDiags)
}
s.ProviderAddr = exampleProviderAddr
s.ProviderSupplyMode = getproviders.ManagedByTerraform
// Test Hash method.
gotHash, diags := s.Hash(exampleStateStoreSchema, exampleProviderSchema, exampleProviderVersion)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}
if gotHash != exampleHash {
t.Fatalf("expected hash for state_store to be %d, but got %d", exampleHash, gotHash)
}
})
// Test cases each change a single input that affects the output hash
// Assertions check that the output hash doesn't match the hash above, following the changed input.
cases := map[string]struct {
config hcl.Body
stateStoreSchema *configschema.Block
providerVersion *version.Version
providerAddr tfaddr.Provider
}{
"changing the state store type affects the hash value": {
config: configBodyForTest(t, `state_store "foobar_CHANGED_VALUE_HERE" {
provider "foobar" {
foobar = "foobar"
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
},
"changing the provider affects the hash value": {
providerAddr: tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "different-provider"),
config: configBodyForTest(t, `state_store "different-provider_fs" {
provider "different-provider" {
foobar = "foobar"
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
},
"changing the provider version affects the hash value": {
providerVersion: version.Must(version.NewSemver("9.9.9")),
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
// If a test case doesn't set an override for these inputs,
// instead use a default value from the example above.
var config hcl.Body
var schema *configschema.Block
var providerVersion *version.Version
var providerAddr tfaddr.Provider
if tc.config == nil {
config = exampleConfig
} else {
config = tc.config
}
if tc.stateStoreSchema == nil {
schema = exampleStateStoreSchema
} else {
schema = tc.stateStoreSchema
}
if tc.providerVersion == nil {
providerVersion = exampleProviderVersion
} else {
providerVersion = tc.providerVersion
}
if tc.providerAddr.IsZero() {
providerAddr = exampleProviderAddr
} else {
providerAddr = tc.providerAddr
}
// Construct a configs.StateStore for the test.
content, _, cfgDiags := config.PartialContent(terraformBlockSchema)
if len(cfgDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", cfgDiags)
}
var ssDiags hcl.Diagnostics
s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0])
if len(ssDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", ssDiags)
}
s.ProviderAddr = providerAddr
s.ProviderSupplyMode = getproviders.ManagedByTerraform
// Test Hash method.
gotHash, diags := s.Hash(schema, exampleProviderSchema, providerVersion)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}
if gotHash == exampleHash {
t.Fatal("expected hash for state_store to be different from the example due to a changed input, but it matched.")
}
})
}
}
func TestStateStore_Hash_edgeCases(t *testing.T) {
// Normally these schemas would come from a provider's GetProviderSchema data
stateStoreSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {
Type: cty.String,
Required: true,
},
"workspace_dir": {
Type: cty.String,
Optional: true,
},
},
}
providerSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foobar": {
Type: cty.String,
Required: true,
},
},
}
providerAddr := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar")
providerVersion := version.Must(version.NewSemver("1.2.3"))
config := configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`)
cases := map[string]struct {
config hcl.Body
providerAddr tfaddr.Provider
providerVersion *version.Version
providerSupplyMode getproviders.ProviderSupplyMode
}{
"tolerates empty config block for the provider even when schema has Required field(s)": {
config: configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
# required field "foobar" is missing
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
providerAddr: providerAddr,
providerVersion: providerVersion,
},
"tolerates missing Required field(s) in state_store config": {
config: configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
}
# required field "path" is missing
workspace_dir = "foobar"
}`),
providerAddr: providerAddr,
providerVersion: providerVersion,
},
"tolerates missing provider version data when using a builtin provider": {
config: config,
providerAddr: tfaddr.NewProvider(tfaddr.BuiltInProviderHost, "hashicorp", "foobar"),
providerVersion: nil, // No version
providerSupplyMode: getproviders.BuiltIn, // Builtin
},
"tolerates missing provider version data when using a reattached provider": {
config: config,
providerAddr: providerAddr,
providerVersion: nil, // No version
providerSupplyMode: getproviders.Reattached,
},
"tolerates missing provider version data when using a dev_override provider": {
config: config,
providerAddr: providerAddr,
providerVersion: nil, // No version
providerSupplyMode: getproviders.DevOverride,
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
// Construct a configs.StateStore for the test.
content, _, cfgDiags := config.PartialContent(terraformBlockSchema)
if len(cfgDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", cfgDiags)
}
var ssDiags hcl.Diagnostics
s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0])
if len(ssDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", ssDiags)
}
s.ProviderAddr = tc.providerAddr
if tc.providerSupplyMode != "" {
s.ProviderSupplyMode = tc.providerSupplyMode
} else {
s.ProviderSupplyMode = getproviders.ManagedByTerraform
}
// Test Hash method.
_, diags := s.Hash(stateStoreSchema, providerSchema, tc.providerVersion)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}
})
}
}
func TestStateStore_Hash_errorConditions(t *testing.T) {
// Normally these schemas would come from a provider's GetProviderSchema data
exampleStateStoreSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {
Type: cty.String,
Required: true,
},
"workspace_dir": {
Type: cty.String,
Optional: true,
},
},
}
exampleProviderSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foobar": {
Type: cty.String,
Required: true,
},
},
}
exampleProviderVersion := version.Must(version.NewSemver("1.2.3"))
// Cases where an error would occur
cases := map[string]struct {
config hcl.Body
stateStoreSchema *configschema.Block
providerVersion *version.Version
providerSupplyMode getproviders.ProviderSupplyMode
wantErrorString string
}{
"returns errors when the state_store config doesn't match the schema": {
providerVersion: exampleProviderVersion,
stateStoreSchema: exampleStateStoreSchema,
config: configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
}
unexpected_block {
foobar = "foobar"
}
unexpected_attr = "foobar"
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
wantErrorString: "Unsupported argument",
},
"returns errors when the provider config doesn't match the schema": {
providerVersion: exampleProviderVersion,
stateStoreSchema: exampleStateStoreSchema,
config: configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
unexpected_attr = "foobar"
unexpected_block {
foobar = "foobar"
}
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
wantErrorString: "Unsupported argument",
},
"returns an error if the state_store schema includes a provider block": {
providerVersion: exampleProviderVersion,
stateStoreSchema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"provider": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
Nesting: configschema.NestingSingle,
},
},
},
config: configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
wantErrorString: `Protected block name "provider" in state store schema`,
},
"returns an error if the state_store schema includes a provider attribute": {
providerVersion: exampleProviderVersion,
stateStoreSchema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"provider": {
Type: cty.String,
Optional: true,
},
},
},
config: configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
wantErrorString: `Protected argument name "provider" in state store schema`,
},
"returns an error if the provider version is missing when using a managed provider": {
providerVersion: nil, // No value provided in this test case
providerSupplyMode: getproviders.ManagedByTerraform,
stateStoreSchema: exampleStateStoreSchema,
config: configBodyForTest(t, `state_store "foobar_fs" {
provider "foobar" {
foobar = "foobar"
}
path = "mystate.tfstate"
workspace_dir = "foobar"
}`),
wantErrorString: `Provider version data was missing during hash generation`,
},
}
for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
// Construct a configs.StateStore for the test.
content, _, cfgDiags := tc.config.PartialContent(terraformBlockSchema)
if len(cfgDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", cfgDiags)
}
var ssDiags hcl.Diagnostics
s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0])
if len(ssDiags) > 0 {
t.Fatalf("unexpected diagnostics: %s", ssDiags)
}
s.ProviderAddr = tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar")
if tc.providerSupplyMode != "" {
s.ProviderSupplyMode = tc.providerSupplyMode
} else {
s.ProviderSupplyMode = getproviders.ManagedByTerraform
}
// Test Hash method.
_, diags := s.Hash(tc.stateStoreSchema, exampleProviderSchema, tc.providerVersion)
if !diags.HasErrors() {
t.Fatal("expected error but got none")
}
if !strings.Contains(diags.Err().Error(), tc.wantErrorString) {
t.Fatalf("expected error to contain %q but got: %s", tc.wantErrorString, diags.Err())
}
})
}
}
func configBodyForTest(t *testing.T, config string) hcl.Body {
t.Helper()
f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
t.Fatalf("failure creating hcl.Body during test setup: %s", diags.Error())
}
return f.Body
}