blob: 780599e1513ecf0b82cd64ea33564077f9620064 [file] [log] [blame]
// 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
}