// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package stackstate import ( "encoding/json" "testing" "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans/planproto" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" "github.com/hashicorp/terraform/internal/states" ) func TestAppliedChangeAsProto(t *testing.T) { tests := map[string]struct { Receiver AppliedChange Want *stacks.AppliedChange }{ "resource instance": { Receiver: &AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "foo"}, Key: addrs.StringKey("beep"), }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "thingy", Name: "thingamajig", }.Instance(addrs.IntKey(1)).Absolute( addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), ), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), }, Schema: providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Required: true, }, "secret": { Type: cty.String, Sensitive: true, }, }, }, }, NewStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), AttrSensitivePaths: []cty.Path{ cty.GetAttrPath("secret"), }, }, }, Want: &stacks.AppliedChange{ Raw: []*stacks.AppliedChange_RawChange{ { Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, Value: mustMarshalAnyPb(t, &tfstackdata1.StateResourceInstanceObjectV1{ ValueJson: json.RawMessage(`{"id":"bar","secret":"top"}`), SensitivePaths: []*planproto.Path{ { Steps: []*planproto.Path_Step{{ Selector: &planproto.Path_Step_AttributeName{AttributeName: "secret"}}}, }, }, ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, Status: tfstackdata1.StateResourceInstanceObjectV1_READY, }), }, }, Descriptions: []*stacks.AppliedChange_ChangeDescription{ { Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ ResourceInstance: &stacks.AppliedChange_ResourceInstance{ Addr: &stacks.ResourceInstanceObjectInStackAddr{ ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, ResourceInstanceAddr: `module.pizza["chicken"].thingy.thingamajig[1]`, }, NewValue: &stacks.DynamicValue{ Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("bar"), "secret": cty.StringVal("top"), }), cty.Object(map[string]cty.Type{"id": cty.String, "secret": cty.String})), Sensitive: []*stacks.AttributePath{{ Steps: []*stacks.AttributePath_Step{{ Selector: &stacks.AttributePath_Step_AttributeName{AttributeName: "secret"}, }}}, }, }, ResourceMode: stacks.ResourceMode_MANAGED, ResourceType: "thingy", ProviderAddr: "example.com/thingers/thingy", }, }, }, }, }, }, "moved_resource instance": { Receiver: &AppliedChangeResourceInstanceObject{ ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "foo"}, Key: addrs.StringKey("beep"), }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "thingy", Name: "thingamajig", }.Instance(addrs.IntKey(1)).Absolute( addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), ), }, }, PreviousResourceInstanceObjectAddr: &stackaddrs.AbsResourceInstanceObject{ Component: stackaddrs.AbsComponentInstance{ Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), Item: stackaddrs.ComponentInstance{ Component: stackaddrs.Component{Name: "foo"}, Key: addrs.StringKey("beep"), }, }, Item: addrs.AbsResourceInstanceObject{ ResourceInstance: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "thingy", Name: "previous_thingamajig", }.Instance(addrs.IntKey(1)).Absolute( addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), ), }, }, ProviderConfigAddr: addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), }, Schema: providers.Schema{ Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Required: true, }, "secret": { Type: cty.String, Sensitive: true, }, }, }, }, NewStateSrc: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), AttrSensitivePaths: []cty.Path{ cty.GetAttrPath("secret"), }, }, }, Want: &stacks.AppliedChange{ Raw: []*stacks.AppliedChange_RawChange{ { Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.previous_thingamajig[1],cur`, Value: nil, }, { Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, Value: mustMarshalAnyPb(t, &tfstackdata1.StateResourceInstanceObjectV1{ ValueJson: json.RawMessage(`{"id":"bar","secret":"top"}`), SensitivePaths: []*planproto.Path{ { Steps: []*planproto.Path_Step{{ Selector: &planproto.Path_Step_AttributeName{AttributeName: "secret"}}}, }, }, ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, Status: tfstackdata1.StateResourceInstanceObjectV1_READY, }), }, }, Descriptions: []*stacks.AppliedChange_ChangeDescription{ { Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.previous_thingamajig[1],cur`, Description: &stacks.AppliedChange_ChangeDescription_Moved{ Moved: &stacks.AppliedChange_Nothing{}, }, }, { Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ ResourceInstance: &stacks.AppliedChange_ResourceInstance{ Addr: &stacks.ResourceInstanceObjectInStackAddr{ ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, ResourceInstanceAddr: `module.pizza["chicken"].thingy.thingamajig[1]`, }, NewValue: &stacks.DynamicValue{ Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("bar"), "secret": cty.StringVal("top"), }), cty.Object(map[string]cty.Type{"id": cty.String, "secret": cty.String})), Sensitive: []*stacks.AttributePath{{ Steps: []*stacks.AttributePath_Step{{ Selector: &stacks.AttributePath_Step_AttributeName{AttributeName: "secret"}, }}}, }, }, ResourceMode: stacks.ResourceMode_MANAGED, ResourceType: "thingy", ProviderAddr: "example.com/thingers/thingy", }, }, }, }, }, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { got, err := test.Receiver.AppliedChangeProto() if err != nil { t.Fatal(err) } if diff := cmp.Diff(test.Want, got, protocmp.Transform()); diff != "" { t.Errorf("wrong result\n%s", diff) } }) } } func mustMarshalAnyPb(t *testing.T, msg proto.Message) *anypb.Any { var ret anypb.Any err := anypb.MarshalFrom(&ret, msg, proto.MarshalOptions{}) if err != nil { t.Fatalf("error marshalling anypb: %q", err) } return &ret } func mustMsgpack(t *testing.T, v cty.Value, ty cty.Type) []byte { t.Helper() ret, err := ctymsgpack.Marshal(v, ty) if err != nil { t.Fatalf("error marshalling %#v: %s", v, err) } return ret }