terraform/internal/states/instance_object_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

156 lines
4.3 KiB
Go
Raw Permalink Normal View History

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package states
import (
2025-02-10 16:13:25 -05:00
"fmt"
"sync"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
2025-02-10 16:13:25 -05:00
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/providers"
"github.com/zclconf/go-cty/cty"
)
func TestResourceInstanceObject_encode(t *testing.T) {
value := cty.ObjectVal(map[string]cty.Value{
"foo": cty.True,
2025-02-10 16:13:25 -05:00
"obj": cty.ObjectVal(map[string]cty.Value{
"sensitive": cty.StringVal("secret").Mark(marks.Sensitive),
}),
"sensitive_a": cty.StringVal("secret").Mark(marks.Sensitive),
"sensitive_b": cty.StringVal("secret").Mark(marks.Sensitive),
})
schema := providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.Bool,
},
"obj": {
Type: cty.Object(map[string]cty.Type{
"sensitive": cty.String,
}),
},
"sensitive_a": {
Type: cty.String,
},
"sensitive_b": {
Type: cty.String,
},
},
},
Version: 0,
}
// The in-memory order of resource dependencies is random, since they're an
// unordered set.
depsOne := []addrs.ConfigResource{
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"),
addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"),
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"),
}
depsTwo := []addrs.ConfigResource{
addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"),
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"),
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"),
}
// multiple instances may have been assigned the same deps slice
objs := []*ResourceInstanceObject{
{
Value: value,
Status: ObjectPlanned,
Dependencies: depsOne,
},
{
Value: value,
Status: ObjectPlanned,
Dependencies: depsTwo,
},
{
Value: value,
Status: ObjectPlanned,
Dependencies: depsOne,
},
{
Value: value,
Status: ObjectPlanned,
Dependencies: depsOne,
},
}
var encoded []*ResourceInstanceObjectSrc
// Encoding can happen concurrently, so we need to make sure the shared
// Dependencies are safely handled
var wg sync.WaitGroup
var mu sync.Mutex
for _, obj := range objs {
wg.Go(func() {
rios, err := obj.Encode(schema)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
mu.Lock()
encoded = append(encoded, rios)
mu.Unlock()
})
}
wg.Wait()
// However, identical sets of dependencies should always be written to state
// in an identical order, so we don't do meaningless state updates on refresh.
for i := 0; i < len(encoded)-1; i++ {
if diff := cmp.Diff(encoded[i].Dependencies, encoded[i+1].Dependencies); diff != "" {
t.Errorf("identical dependencies got encoded in different orders:\n%s", diff)
}
}
2025-02-10 16:13:25 -05:00
// sensitive paths must also be consistent got comparison
for i := 0; i < len(encoded)-1; i++ {
a, b := fmt.Sprintf("%#v", encoded[i].AttrSensitivePaths), fmt.Sprintf("%#v", encoded[i+1].AttrSensitivePaths)
if diff := cmp.Diff(a, b); diff != "" {
t.Errorf("sensitive paths got encoded in different orders:\n%s", diff)
}
}
}
func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) {
value := cty.ObjectVal(map[string]cty.Value{
// State only supports a subset of marks that we know how to persist
// between plan/apply rounds. All values with other marks must be
// replaced with unmarked placeholders before attempting to store the
// value in the state.
"foo": cty.True.Mark("unsupported"),
})
schema := providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.Bool,
},
},
},
Version: 0,
}
obj := &ResourceInstanceObject{
Value: value,
Status: ObjectReady,
}
_, err := obj.Encode(schema)
if err == nil {
t.Fatalf("unexpected success; want error")
}
got := err.Error()
Handle marks a little more consistently In the very first implementation of "sensitive values" we were unfortunately not disciplined about separating the idea of "marked value" from the idea of "sensitive value" (where the latter is a subset of the former). The first implementation just assumed that any marking whatsoever meant "sensitive". We later improved that by adding the marks package and the marks.Sensitive value to standardize on the representation of "sensitive value" as being a value marked with _that specific mark_. However, we did not perform a thorough review of all of the mark-handling codepaths to make sure they all agreed on that definition. In particular, the state and plan models were both designed as if they supported arbitrary marks but then in practice marks other than marks.Sensitive would be handled in various inconsistent ways: dropped entirely, or interpreted as if marks.Sensitive, and possibly do so inconsistently when a value is used only in memory vs. round-tripped through a wire/file format. The goal of this commit is to resolve those oddities so that there are now two possible situations: - General mark handling: some codepaths genuinely handle marks generically, by transporting them from input value to output value in a way consistent with how cty itself deals with marks. This is the ideal case because it means we can add new marks in future and assume these codepaths will handle them correctly without any further modifications. - Sensitive-only mark preservation: the codepaths that interact with our wire protocols and file formats typically have only specialized support for sensitive values in particular, and lack support for any other marks. Those codepaths are now subject to a new rule where they must return an error if asked to deal with any other mark, so that if we introduce new marks in future we'll be forced either to define how we'll avoid those markings reaching the file/wire formats or extend the file/wire formats to support the new marks. Some new helper functions in package marks are intended to standardize how we deal with the "sensitive values only" situations, in the hope that this will make it easier to keep things consistent as the codebase evolves in future. In practice the modules runtime only ever uses marks.Sensitive as a mark today, so all of these checks are effectively covering "should never happen" cases. The only other mark Terraform uses is an implementation detail of "terraform console" and does not interact with any of the codepaths that only support sensitive values in particular.
2024-04-16 20:20:33 -04:00
want := `.foo: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)`
if got != want {
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
}
}