mirror of https://github.com/hashicorp/terraform
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.
441 lines
14 KiB
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
|
|
}
|