mirror of https://github.com/hashicorp/terraform
PSS: Add ability to create hashes of the `state_store` block and of its nested `provider` block (#37278)
* Add the ability to make a hash of state store config * Add test demonstrating that the provider block doesn't impact the hash of a state_store block * Make sure test asserts what would happen if the schema DID include the provider block * Update the Hash method to return diagnostics, ignore nested provider blocks, and validate incoming schema and config * Update tests to use more representative config, fix code under test as a result * Update Hash method to return hashes for both the state_store and provider blocks * Add test cases to cover how required fields are tolerated when making the hash This is because ENVs may supply those values. * Fix inaccurate comments * Add test to show that hashes are consistent and exclude the provider block * Update backend state file to contain hash of provider block's config * Fix test to expect a hash for the provider config block. * Fix bug in DeepCopy method, update test to have better error messages when diffs are detected * Update test to explicitly check hash values * Try make test intention clearer * Improve user feedback when state store schema contains the protected word "provider" * Update tests * Update test to test the Hash method in a more true-to-life way Copy of 04a1201878cd1f6f117c43c43c1ee9d0fc17cec1 by Radek Simko * Update test to use new approach * Fix `TestInit_stateStoreBlockIsExperimental` test failurepull/37326/head
parent
92609fded1
commit
7a27366b39
@ -0,0 +1,280 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"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) {
|
||||
|
||||
// This test assumes a configuration like this,
|
||||
// where the "fs" state store is implemented in
|
||||
// the "foobar" provider:
|
||||
//
|
||||
// terraform {
|
||||
// required_providers = {
|
||||
// # entries would be here
|
||||
// }
|
||||
// state_store "foobar_fs" {
|
||||
// # Nested provider block
|
||||
// provider "foobar" {
|
||||
// foobar = "foobar"
|
||||
// }
|
||||
|
||||
// # Attributes for configuring the state store
|
||||
// path = "mystate.tfstate"
|
||||
// workspace_dir = "foobar"
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
config hcl.Body
|
||||
providerConfig hcl.Body
|
||||
schema *configschema.Block
|
||||
wantErrorString string
|
||||
wantProviderHash int
|
||||
wantStateStoreHash int
|
||||
}{
|
||||
"ignores the provider block in config data, as long as the schema doesn't include it": {
|
||||
schema: stateStoreSchema,
|
||||
config: configBodyForTest(t, `state_store "foo" {
|
||||
provider "foobar" {
|
||||
foobar = "foobar"
|
||||
}
|
||||
path = "mystate.tfstate"
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
providerConfig: configBodyForTest(t, `foobar = "foobar"`),
|
||||
wantProviderHash: 2672365208,
|
||||
wantStateStoreHash: 3037430836,
|
||||
},
|
||||
"tolerates empty config block for the provider even when schema has Required field(s)": {
|
||||
schema: stateStoreSchema,
|
||||
config: configBodyForTest(t, `state_store "foo" {
|
||||
provider "foobar" {
|
||||
# required field "foobar" is missing
|
||||
}
|
||||
path = "mystate.tfstate"
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
providerConfig: hcl.EmptyBody(),
|
||||
wantProviderHash: 2911589008,
|
||||
wantStateStoreHash: 3037430836,
|
||||
},
|
||||
"tolerates missing Required field(s) in state_store config": {
|
||||
schema: stateStoreSchema,
|
||||
config: configBodyForTest(t, `state_store "foo" {
|
||||
provider "foobar" {
|
||||
foobar = "foobar"
|
||||
}
|
||||
|
||||
# required field "path" is missing
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
providerConfig: configBodyForTest(t, `foobar = "foobar"`),
|
||||
wantProviderHash: 2672365208,
|
||||
wantStateStoreHash: 3453024478,
|
||||
},
|
||||
"returns errors when the config contains non-provider things that aren't in the schema": {
|
||||
schema: stateStoreSchema,
|
||||
config: configBodyForTest(t, `state_store "foo" {
|
||||
provider "foobar" {
|
||||
foobar = "foobar"
|
||||
}
|
||||
unexpected_block {
|
||||
foobar = "foobar"
|
||||
}
|
||||
unexpected_attr = "foobar"
|
||||
path = "mystate.tfstate"
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
providerConfig: configBodyForTest(t, `foobar = "foobar"`),
|
||||
wantErrorString: "Unsupported argument",
|
||||
},
|
||||
"returns an error if the schema includes a provider block": {
|
||||
schema: &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 "foo" {
|
||||
provider "foobar" {
|
||||
foobar = "foobar"
|
||||
}
|
||||
path = "mystate.tfstate"
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
providerConfig: configBodyForTest(t, `foobar = "foobar"`),
|
||||
wantErrorString: `Protected block name "provider" in state store schema`,
|
||||
},
|
||||
"returns an error if the schema includes a provider attribute": {
|
||||
schema: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"provider": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
config: configBodyForTest(t, `state_store "foo" {
|
||||
provider "foobar" {
|
||||
foobar = "foobar"
|
||||
}
|
||||
path = "mystate.tfstate"
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
providerConfig: configBodyForTest(t, `foobar = "foobar"`),
|
||||
wantErrorString: `Protected argument name "provider" in state store schema`,
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
t.Run(tn, func(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
ssHash, pHash, diags := s.Hash(tc.schema, providerSchema)
|
||||
if diags.HasErrors() {
|
||||
if tc.wantErrorString == "" {
|
||||
t.Fatalf("unexpected error: %s", diags.Err())
|
||||
}
|
||||
if !strings.Contains(diags.Err().Error(), tc.wantErrorString) {
|
||||
t.Fatalf("expected %q to be in the returned error string but it's missing: %q", tc.wantErrorString, diags.Err())
|
||||
}
|
||||
|
||||
return // early return if testing an error case
|
||||
}
|
||||
|
||||
if !diags.HasErrors() && tc.wantErrorString != "" {
|
||||
t.Fatal("expected an error when generating a hash, but got none")
|
||||
}
|
||||
|
||||
if ssHash != tc.wantStateStoreHash {
|
||||
t.Fatalf("expected hash for state_store to be %d, but got %d", tc.wantStateStoreHash, ssHash)
|
||||
}
|
||||
if pHash != tc.wantProviderHash {
|
||||
t.Fatalf("expected hash for provider to be %d, but got %d", tc.wantProviderHash, pHash)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateStore_checkStateStoreHashUnaffectedByProviderBlock(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,
|
||||
},
|
||||
},
|
||||
}
|
||||
providerConfig := configBodyForTest(t, `foobar = "foobar"`)
|
||||
|
||||
// Make two StateStores:
|
||||
// 1) Has provider block in the main Config value, as well as matching data in Provider.Config
|
||||
// 2) Doesn't have provider block in the main Config value, has config in Provider.Config
|
||||
|
||||
s1 := StateStore{
|
||||
Config: configBodyForTest(t, `state_store "foo" {
|
||||
provider "foobar" {
|
||||
foobar = "foobar"
|
||||
}
|
||||
path = "mystate.tfstate"
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
Provider: &Provider{
|
||||
Config: providerConfig,
|
||||
},
|
||||
}
|
||||
s2 := StateStore{
|
||||
Config: configBodyForTest(t, `state_store "foo" {
|
||||
# No provider block here
|
||||
|
||||
path = "mystate.tfstate"
|
||||
workspace_dir = "foobar"
|
||||
}`),
|
||||
Provider: &Provider{
|
||||
Config: providerConfig,
|
||||
},
|
||||
}
|
||||
|
||||
s1StoreHash, _, _ := s1.Hash(stateStoreSchema, providerSchema)
|
||||
s2StoreHash, _, _ := s2.Hash(stateStoreSchema, providerSchema)
|
||||
|
||||
if s1StoreHash != s2StoreHash {
|
||||
t.Fatalf("expected state_store block hashes to match, as hashing logic should ignore presence of provider block. Got s1 %d, s2 %d", s1StoreHash, s2StoreHash)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
return f.Body
|
||||
}
|
||||
Loading…
Reference in new issue