opentofu/internal/command/jsonplan/values_test.go
Kristin Laemmert ac03d35997
jsonplan and jsonstate: include sensitive_values in state representations (#28889)
* jsonplan and jsonstate: include sensitive_values in state representations

A sensitive_values field has been added to the resource in state and planned values which is a map of all sensitive attributes with the values set to true.

It wasn't entirely clear to me if the values in state would suffice, or if we also need to consult the schema - I believe that this is sufficient for state files written since v0.15, and if that's incorrect or insufficient, I'll add in the provider schema check as well.

I also updated the documentation, and, since we've considered this before, bumped the FormatVersions for both jsonstate and jsonplan.
2021-06-14 09:19:13 -04:00

374 lines
8.5 KiB
Go

package jsonplan
import (
"encoding/json"
"reflect"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestMarshalAttributeValues(t *testing.T) {
tests := []struct {
Attr cty.Value
Schema *configschema.Block
Want attributeValues
}{
{
cty.NilVal,
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
nil,
},
{
cty.NullVal(cty.String),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
nil,
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
attributeValues{"foo": json.RawMessage(`"bar"`)},
},
{
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.String),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {
Type: cty.String,
Optional: true,
},
},
},
attributeValues{"foo": json.RawMessage(`null`)},
},
{
cty.ObjectVal(map[string]cty.Value{
"bar": cty.MapVal(map[string]cty.Value{
"hello": cty.StringVal("world"),
}),
"baz": cty.ListVal([]cty.Value{
cty.StringVal("goodnight"),
cty.StringVal("moon"),
}),
}),
&configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.Map(cty.String),
Required: true,
},
"baz": {
Type: cty.List(cty.String),
Optional: true,
},
},
},
attributeValues{
"bar": json.RawMessage(`{"hello":"world"}`),
"baz": json.RawMessage(`["goodnight","moon"]`),
},
},
}
for _, test := range tests {
got := marshalAttributeValues(test.Attr, test.Schema)
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}
func TestMarshalPlannedOutputs(t *testing.T) {
after, _ := plans.NewDynamicValue(cty.StringVal("after"), cty.DynamicPseudoType)
tests := []struct {
Changes *plans.Changes
Want map[string]output
Err bool
}{
{
&plans.Changes{},
nil,
false,
},
{
&plans.Changes{
Outputs: []*plans.OutputChangeSrc{
{
Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
After: after,
},
Sensitive: false,
},
},
},
map[string]output{
"bar": {
Sensitive: false,
Value: json.RawMessage(`"after"`),
},
},
false,
},
{ // Delete action
&plans.Changes{
Outputs: []*plans.OutputChangeSrc{
{
Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
},
Sensitive: false,
},
},
},
map[string]output{},
false,
},
}
for _, test := range tests {
got, err := marshalPlannedOutputs(test.Changes)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}
func TestMarshalPlanResources(t *testing.T) {
tests := map[string]struct {
Action plans.Action
Before cty.Value
After cty.Value
Want []resource
Err bool
}{
"create with unknowns": {
Action: plans.Create,
Before: cty.NullVal(cty.EmptyObject),
After: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.UnknownVal(cty.String),
"foozles": cty.UnknownVal(cty.String),
}),
Want: []resource{{
Address: "test_thing.example",
Mode: "managed",
Type: "test_thing",
Name: "example",
Index: addrs.InstanceKey(nil),
ProviderName: "registry.terraform.io/hashicorp/test",
SchemaVersion: 1,
AttributeValues: attributeValues{},
SensitiveValues: json.RawMessage("{}"),
}},
Err: false,
},
"delete with null and nil": {
Action: plans.Delete,
Before: cty.NullVal(cty.EmptyObject),
After: cty.NilVal,
Want: nil,
Err: false,
},
"delete": {
Action: plans.Delete,
Before: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.StringVal("foo"),
"foozles": cty.StringVal("bar"),
}),
After: cty.NullVal(cty.Object(map[string]cty.Type{
"woozles": cty.String,
"foozles": cty.String,
})),
Want: nil,
Err: false,
},
"update without unknowns": {
Action: plans.Update,
Before: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.StringVal("foo"),
"foozles": cty.StringVal("bar"),
}),
After: cty.ObjectVal(map[string]cty.Value{
"woozles": cty.StringVal("baz"),
"foozles": cty.StringVal("bat"),
}),
Want: []resource{{
Address: "test_thing.example",
Mode: "managed",
Type: "test_thing",
Name: "example",
Index: addrs.InstanceKey(nil),
ProviderName: "registry.terraform.io/hashicorp/test",
SchemaVersion: 1,
AttributeValues: attributeValues{
"woozles": json.RawMessage(`"baz"`),
"foozles": json.RawMessage(`"bat"`),
},
SensitiveValues: json.RawMessage("{}"),
}},
Err: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
before, err := plans.NewDynamicValue(test.Before, test.Before.Type())
if err != nil {
t.Fatal(err)
}
after, err := plans.NewDynamicValue(test.After, test.After.Type())
if err != nil {
t.Fatal(err)
}
testChange := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "example",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
ProviderAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: test.Action,
Before: before,
After: after,
},
},
},
}
ris := testResourceAddrs()
got, err := marshalPlanResources(testChange, ris, testSchemas())
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
eq := reflect.DeepEqual(got, test.Want)
if !eq {
t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
})
}
}
func TestMarshalPlanValuesNoopDeposed(t *testing.T) {
dynamicNull, err := plans.NewDynamicValue(cty.NullVal(cty.DynamicPseudoType), cty.DynamicPseudoType)
if err != nil {
t.Fatal(err)
}
testChange := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "example",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
DeposedKey: "12345678",
ProviderAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: plans.NoOp,
Before: dynamicNull,
After: dynamicNull,
},
},
},
}
_, err = marshalPlannedValues(testChange, testSchemas())
if err != nil {
t.Fatal(err)
}
}
func testSchemas() *terraform.Schemas {
return &terraform.Schemas{
Providers: map[addrs.Provider]*terraform.ProviderSchema{
addrs.NewDefaultProvider("test"): &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_thing": {
Attributes: map[string]*configschema.Attribute{
"woozles": {Type: cty.String, Optional: true, Computed: true},
"foozles": {Type: cty.String, Optional: true},
},
},
},
ResourceTypeSchemaVersions: map[string]uint64{
"test_thing": 1,
},
},
},
}
}
func testResourceAddrs() []addrs.AbsResourceInstance {
return []addrs.AbsResourceInstance{
mustAddr("test_thing.example"),
}
}
func mustAddr(str string) addrs.AbsResourceInstance {
addr, diags := addrs.ParseAbsResourceInstanceStr(str)
if diags.HasErrors() {
panic(diags.Err())
}
return addr
}