| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package stackplan |
| |
| import ( |
| "testing" |
| "time" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/hashicorp/go-version" |
| "github.com/zclconf/go-cty/cty" |
| "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/lang/marks" |
| "github.com/hashicorp/terraform/internal/plans" |
| "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" |
| ) |
| |
| func TestPlannedChangeAsProto(t *testing.T) { |
| emptyObjectForPlan, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) |
| if err != nil { |
| t.Fatal(err) |
| } |
| nonEmptyType := cty.Map(cty.String) |
| beforeObjectForPlan, err := plans.NewDynamicValue(cty.MapVal(map[string]cty.Value{ |
| "foo": cty.StringVal("bar"), |
| }), nonEmptyType) |
| if err != nil { |
| t.Fatal(err) |
| } |
| afterObjectForPlan, err := plans.NewDynamicValue(cty.MapVal(map[string]cty.Value{ |
| "foo": cty.StringVal("baz"), |
| }), nonEmptyType) |
| if err != nil { |
| t.Fatal(err) |
| } |
| nullObjectForPlan, err := plans.NewDynamicValue(cty.NullVal(cty.EmptyObject), cty.EmptyObject) |
| if err != nil { |
| t.Fatal(err) |
| } |
| fakePlanTimestamp, err := time.Parse(time.RFC3339, "2017-03-27T10:00:00-08:00") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| tests := map[string]struct { |
| Receiver PlannedChange |
| Want *stacks.PlannedChange |
| WantErr string |
| }{ |
| "header": { |
| Receiver: &PlannedChangeHeader{ |
| TerraformVersion: version.Must(version.NewSemver("1.2.3-beta4")), |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanHeader{ |
| TerraformVersion: "1.2.3-beta4", |
| }), |
| }, |
| }, |
| }, |
| "applyable true": { |
| Receiver: &PlannedChangeApplyable{ |
| Applyable: true, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanApplyable{ |
| Applyable: true, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_PlanApplyable{ |
| PlanApplyable: true, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "applyable false": { |
| Receiver: &PlannedChangeApplyable{ |
| Applyable: false, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanApplyable{ |
| // false is the default |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_PlanApplyable{ |
| PlanApplyable: false, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "component instance create": { |
| Receiver: &PlannedChangeComponentInstance{ |
| Addr: stackaddrs.AbsComponentInstance{ |
| Stack: stackaddrs.RootStackInstance, |
| Item: stackaddrs.ComponentInstance{ |
| Component: stackaddrs.Component{Name: "foo"}, |
| }, |
| }, |
| Action: plans.Create, |
| PlanTimestamp: fakePlanTimestamp, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{ |
| ComponentInstanceAddr: "component.foo", |
| PlanTimestamp: "2017-03-27T10:00:00-08:00", |
| PlannedAction: planproto.Action_CREATE, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{ |
| ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{ |
| Addr: &stacks.ComponentInstanceInStackAddr{ |
| ComponentAddr: "component.foo", |
| ComponentInstanceAddr: "component.foo", |
| }, |
| Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "component instance noop": { |
| Receiver: &PlannedChangeComponentInstance{ |
| Addr: stackaddrs.AbsComponentInstance{ |
| Stack: stackaddrs.RootStackInstance, |
| Item: stackaddrs.ComponentInstance{ |
| Component: stackaddrs.Component{Name: "foo"}, |
| Key: addrs.StringKey("bar"), |
| }, |
| }, |
| Action: plans.NoOp, |
| PlanTimestamp: fakePlanTimestamp, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{ |
| ComponentInstanceAddr: `component.foo["bar"]`, |
| PlanTimestamp: "2017-03-27T10:00:00-08:00", |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{ |
| ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{ |
| Actions: []stacks.ChangeType{stacks.ChangeType_NOOP}, |
| Addr: &stacks.ComponentInstanceInStackAddr{ |
| ComponentAddr: "component.foo", |
| ComponentInstanceAddr: `component.foo["bar"]`, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "component instance delete": { |
| Receiver: &PlannedChangeComponentInstance{ |
| Addr: stackaddrs.AbsComponentInstance{ |
| Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), |
| Item: stackaddrs.ComponentInstance{ |
| Component: stackaddrs.Component{Name: "foo"}, |
| }, |
| }, |
| Action: plans.Delete, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo`, |
| PlannedAction: planproto.Action_DELETE, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{ |
| ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{ |
| Addr: &stacks.ComponentInstanceInStackAddr{ |
| ComponentAddr: "stack.a.component.foo", |
| ComponentInstanceAddr: `stack.a["boop"].component.foo`, |
| }, |
| Actions: []stacks.ChangeType{stacks.ChangeType_DELETE}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "resource instance deferred": { |
| Receiver: &PlannedChangeDeferredResourceInstancePlanned{ |
| ResourceInstancePlanned: PlannedChangeResourceInstancePlanned{ |
| 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: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| DeposedKey: addrs.DeposedKey("aaaaaaaa"), |
| }, |
| }, |
| ProviderConfigAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: &plans.ResourceInstanceChangeSrc{ |
| Addr: addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "thingy", |
| Name: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| DeposedKey: addrs.DeposedKey("aaaaaaaa"), |
| ProviderAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: plans.ChangeSrc{ |
| Action: plans.Create, |
| Before: nullObjectForPlan, |
| After: emptyObjectForPlan, |
| }, |
| }, |
| }, |
| DeferredReason: providers.DeferredReasonResourceConfigUnknown, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanDeferredResourceInstanceChange{ |
| Change: &tfstackdata1.PlanResourceInstanceChangePlanned{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, |
| Change: &planproto.ResourceInstanceChange{ |
| Addr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| Change: &planproto.Change{ |
| Action: planproto.Action_CREATE, |
| Values: []*planproto.DynamicValue{ |
| {Msgpack: []byte{'\x80'}}, // zero-length mapping |
| }, |
| }, |
| Provider: `provider["example.com/thingers/thingy"]`, |
| }, |
| }, |
| Deferred: &planproto.Deferred{ |
| Reason: planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN, |
| }, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ResourceInstanceDeferred{ |
| ResourceInstanceDeferred: &stacks.PlannedChange_ResourceInstanceDeferred{ |
| ResourceInstance: &stacks.PlannedChange_ResourceInstance{ |
| Addr: &stacks.ResourceInstanceObjectInStackAddr{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| }, |
| ResourceMode: stacks.ResourceMode_MANAGED, |
| ResourceType: "thingy", |
| ProviderAddr: "example.com/thingers/thingy", |
| Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, |
| ActionReason: "ResourceInstanceChangeNoReason", |
| Index: &stacks.PlannedChange_ResourceInstance_Index{ |
| Value: &stacks.DynamicValue{ |
| Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 |
| }, |
| }, |
| ModuleAddr: `module.pizza["chicken"]`, |
| ResourceName: "wotsit", |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: []byte{'\xc0'}, // null |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: []byte{'\x80'}, // zero-length mapping |
| }, |
| }, |
| }, |
| Deferred: &stacks.Deferred{ |
| Reason: stacks.Deferred_RESOURCE_CONFIG_UNKNOWN, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "resource instance planned create": { |
| Receiver: &PlannedChangeResourceInstancePlanned{ |
| 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: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| DeposedKey: addrs.DeposedKey("aaaaaaaa"), |
| }, |
| }, |
| ProviderConfigAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: &plans.ResourceInstanceChangeSrc{ |
| Addr: addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "thingy", |
| Name: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| DeposedKey: addrs.DeposedKey("aaaaaaaa"), |
| ProviderAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: plans.ChangeSrc{ |
| Action: plans.Create, |
| Before: nullObjectForPlan, |
| After: emptyObjectForPlan, |
| }, |
| }, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, |
| Change: &planproto.ResourceInstanceChange{ |
| Addr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| Change: &planproto.Change{ |
| Action: planproto.Action_CREATE, |
| Values: []*planproto.DynamicValue{ |
| {Msgpack: []byte{'\x80'}}, // zero-length mapping |
| }, |
| }, |
| Provider: `provider["example.com/thingers/thingy"]`, |
| }, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ |
| ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ |
| Addr: &stacks.ResourceInstanceObjectInStackAddr{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| }, |
| ResourceMode: stacks.ResourceMode_MANAGED, |
| ResourceType: "thingy", |
| ProviderAddr: "example.com/thingers/thingy", |
| Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, |
| ActionReason: "ResourceInstanceChangeNoReason", |
| Index: &stacks.PlannedChange_ResourceInstance_Index{ |
| Value: &stacks.DynamicValue{ |
| Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 |
| }, |
| }, |
| ModuleAddr: `module.pizza["chicken"]`, |
| ResourceName: "wotsit", |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: []byte{'\xc0'}, // null |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: []byte{'\x80'}, // zero-length mapping |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "resource instance planned replace": { |
| Receiver: &PlannedChangeResourceInstancePlanned{ |
| 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: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| DeposedKey: addrs.DeposedKey("aaaaaaaa"), |
| }, |
| }, |
| ProviderConfigAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: &plans.ResourceInstanceChangeSrc{ |
| Addr: addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "thingy", |
| Name: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| DeposedKey: addrs.DeposedKey("aaaaaaaa"), |
| ProviderAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: plans.ChangeSrc{ |
| Action: plans.DeleteThenCreate, |
| Before: beforeObjectForPlan, |
| After: afterObjectForPlan, |
| }, |
| RequiredReplace: cty.NewPathSet(cty.GetAttrPath("foo")), |
| }, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, |
| Change: &planproto.ResourceInstanceChange{ |
| Addr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| Change: &planproto.Change{ |
| Action: planproto.Action_DELETE_THEN_CREATE, |
| Values: []*planproto.DynamicValue{ |
| {Msgpack: []byte("\x81\xa3foo\xa3bar")}, |
| {Msgpack: []byte("\x81\xa3foo\xa3baz")}, |
| }, |
| }, |
| Provider: `provider["example.com/thingers/thingy"]`, |
| RequiredReplace: []*planproto.Path{ |
| { |
| Steps: []*planproto.Path_Step{ |
| { |
| Selector: &planproto.Path_Step_AttributeName{AttributeName: "foo"}, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ |
| ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ |
| Addr: &stacks.ResourceInstanceObjectInStackAddr{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| DeposedKey: "aaaaaaaa", |
| }, |
| ResourceMode: stacks.ResourceMode_MANAGED, |
| ResourceType: "thingy", |
| ProviderAddr: "example.com/thingers/thingy", |
| Actions: []stacks.ChangeType{stacks.ChangeType_DELETE, stacks.ChangeType_CREATE}, |
| ActionReason: "ResourceInstanceChangeNoReason", |
| Index: &stacks.PlannedChange_ResourceInstance_Index{ |
| Value: &stacks.DynamicValue{ |
| Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 |
| }, |
| }, |
| ModuleAddr: `module.pizza["chicken"]`, |
| ResourceName: "wotsit", |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: []byte("\x81\xa3foo\xa3bar"), |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: []byte("\x81\xa3foo\xa3baz"), |
| }, |
| }, |
| ReplacePaths: []*stacks.AttributePath{ |
| { |
| Steps: []*stacks.AttributePath_Step{ |
| { |
| Selector: &stacks.AttributePath_Step_AttributeName{ |
| AttributeName: "foo", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "resource instance planned import": { |
| Receiver: &PlannedChangeResourceInstancePlanned{ |
| 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: "wotsit", |
| }.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"), |
| }, |
| ChangeSrc: &plans.ResourceInstanceChangeSrc{ |
| Addr: addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "thingy", |
| Name: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| ProviderAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: plans.ChangeSrc{ |
| Action: plans.NoOp, |
| Before: emptyObjectForPlan, |
| After: emptyObjectForPlan, |
| Importing: &plans.ImportingSrc{ |
| ID: "bbbbbbb", |
| }, |
| }, |
| }, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, |
| Change: &planproto.ResourceInstanceChange{ |
| Addr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| Change: &planproto.Change{ |
| Action: planproto.Action_NOOP, |
| Values: []*planproto.DynamicValue{ |
| {Msgpack: []byte{'\x80'}}, // zero-length mapping |
| }, |
| Importing: &planproto.Importing{ |
| Id: "bbbbbbb", |
| }, |
| }, |
| Provider: `provider["example.com/thingers/thingy"]`, |
| }, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ |
| ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ |
| Addr: &stacks.ResourceInstanceObjectInStackAddr{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| }, |
| ResourceMode: stacks.ResourceMode_MANAGED, |
| ResourceType: "thingy", |
| ProviderAddr: "example.com/thingers/thingy", |
| Actions: []stacks.ChangeType{stacks.ChangeType_NOOP}, |
| ActionReason: "ResourceInstanceChangeNoReason", |
| Index: &stacks.PlannedChange_ResourceInstance_Index{ |
| Value: &stacks.DynamicValue{ |
| Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 |
| }, |
| }, |
| ModuleAddr: `module.pizza["chicken"]`, |
| ResourceName: "wotsit", |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: []byte{'\x80'}, // zero-length mapping |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: []byte{'\x80'}, // zero-length mapping |
| }, |
| }, |
| Imported: &stacks.PlannedChange_ResourceInstance_Imported{ |
| ImportId: "bbbbbbb", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "resource instance planned moved": { |
| Receiver: &PlannedChangeResourceInstancePlanned{ |
| 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: "wotsit", |
| }.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"), |
| }, |
| ChangeSrc: &plans.ResourceInstanceChangeSrc{ |
| Addr: addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "thingy", |
| Name: "wotsit", |
| }.Instance(addrs.IntKey(1)).Absolute( |
| addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| ), |
| PrevRunAddr: addrs.AbsResourceInstance{ |
| Resource: addrs.Resource{ |
| Mode: addrs.ManagedResourceMode, |
| Type: "thingy", |
| Name: "wotsit", |
| }.Instance(addrs.NoKey), |
| Module: addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), |
| }, |
| ProviderAddr: addrs.AbsProviderConfig{ |
| Module: addrs.RootModule, |
| Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), |
| }, |
| ChangeSrc: plans.ChangeSrc{ |
| Action: plans.NoOp, |
| Before: emptyObjectForPlan, |
| After: emptyObjectForPlan, |
| }, |
| }, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, |
| Change: &planproto.ResourceInstanceChange{ |
| Addr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| PrevRunAddr: `module.pizza["chicken"].thingy.wotsit`, |
| Change: &planproto.Change{ |
| Action: planproto.Action_NOOP, |
| Values: []*planproto.DynamicValue{ |
| {Msgpack: []byte{'\x80'}}, // zero-length mapping |
| }, |
| }, |
| Provider: `provider["example.com/thingers/thingy"]`, |
| }, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ |
| ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ |
| Addr: &stacks.ResourceInstanceObjectInStackAddr{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, |
| }, |
| ResourceMode: stacks.ResourceMode_MANAGED, |
| ResourceType: "thingy", |
| ProviderAddr: "example.com/thingers/thingy", |
| Actions: []stacks.ChangeType{stacks.ChangeType_NOOP}, |
| ActionReason: "ResourceInstanceChangeNoReason", |
| Index: &stacks.PlannedChange_ResourceInstance_Index{ |
| Value: &stacks.DynamicValue{ |
| Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 |
| }, |
| }, |
| ModuleAddr: `module.pizza["chicken"]`, |
| ResourceName: "wotsit", |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: []byte{'\x80'}, // zero-length mapping |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: []byte{'\x80'}, // zero-length mapping |
| }, |
| }, |
| Moved: &stacks.PlannedChange_ResourceInstance_Moved{ |
| PrevAddr: &stacks.ResourceInstanceInStackAddr{ |
| ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, |
| ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit`, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "output value updated": { |
| Receiver: &PlannedChangeOutputValue{ |
| Addr: stackaddrs.OutputValue{Name: "thingy_id"}, |
| Action: plans.Update, |
| |
| // NOTE: This is a bit unrealistic since we're reporting an |
| // update but there's no difference between these two values. |
| // In a real planned change this situation would be a "no-op". |
| Before: cty.EmptyObjectVal, |
| After: cty.EmptyObjectVal, |
| }, |
| Want: &stacks.PlannedChange{ |
| // Output value changes don't generate any raw representation; |
| // the diff is only for the benefit of the operator and |
| // other subsystems operating on their behalf. |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_OutputValuePlanned{ |
| OutputValuePlanned: &stacks.PlannedChange_OutputValue{ |
| Name: "thingy_id", |
| Actions: []stacks.ChangeType{stacks.ChangeType_UPDATE}, |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.EmptyObjectVal), |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.EmptyObjectVal), |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "create sensitive root input variable": { |
| Receiver: &PlannedChangeRootInputValue{ |
| Addr: stackaddrs.InputVariable{Name: "thingy_id"}, |
| Action: plans.Create, |
| Before: cty.NullVal(cty.String), |
| After: cty.StringVal("boop").Mark(marks.Sensitive), |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ |
| Name: "thingy_id", |
| Value: &tfstackdata1.DynamicValue{ |
| Value: &planproto.DynamicValue{ |
| Msgpack: []byte("\x92\xc4\b\"string\"\xa4boop"), |
| }, |
| SensitivePaths: []*planproto.Path{ |
| { |
| Steps: make([]*planproto.Path_Step, 0), // no steps as it is the root value |
| }, |
| }, |
| }, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ |
| InputVariablePlanned: &stacks.PlannedChange_InputVariable{ |
| Name: "thingy_id", |
| Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.StringVal("boop")), |
| Sensitive: []*stacks.AttributePath{ |
| { |
| Steps: make([]*stacks.AttributePath_Step, 0), // no steps as it is the root value |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "ephemeral root input variable": { |
| Receiver: &PlannedChangeRootInputValue{ |
| Addr: stackaddrs.InputVariable{Name: "thingy_id"}, |
| Action: plans.Create, |
| Before: cty.NullVal(cty.String), |
| After: cty.StringVal("boop").Mark(marks.Ephemeral), |
| }, |
| WantErr: "failed to encode after planned input variable var.thingy_id: : unhandled value marks cty.NewValueMarks(marks.Ephemeral) (this is a bug in Terraform)", // Ephemeral values should never make it this far. |
| }, |
| "update root input variable": { |
| Receiver: &PlannedChangeRootInputValue{ |
| Addr: stackaddrs.InputVariable{Name: "thingy_id"}, |
| Action: plans.Update, |
| Before: cty.StringVal("beep"), |
| After: cty.StringVal("boop"), |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ |
| Name: "thingy_id", |
| Value: &tfstackdata1.DynamicValue{ |
| Value: &planproto.DynamicValue{ |
| Msgpack: []byte("\x92\xc4\b\"string\"\xa4boop"), |
| }, |
| }, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ |
| InputVariablePlanned: &stacks.PlannedChange_InputVariable{ |
| Name: "thingy_id", |
| Actions: []stacks.ChangeType{stacks.ChangeType_UPDATE}, |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.StringVal("beep")), |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.StringVal("boop")), |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| "root input variable that must be re-supplied during apply": { |
| Receiver: &PlannedChangeRootInputValue{ |
| Addr: stackaddrs.InputVariable{Name: "thingy_id"}, |
| Action: plans.Create, |
| Before: cty.NullVal(cty.String), |
| After: cty.NullVal(cty.String), |
| RequiredOnApply: true, |
| }, |
| Want: &stacks.PlannedChange{ |
| Raw: []*anypb.Any{ |
| mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ |
| Name: "thingy_id", |
| Value: &tfstackdata1.DynamicValue{ |
| Value: &planproto.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), |
| }, |
| }, |
| RequiredOnApply: true, |
| }), |
| }, |
| Descriptions: []*stacks.PlannedChange_ChangeDescription{ |
| { |
| Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ |
| InputVariablePlanned: &stacks.PlannedChange_InputVariable{ |
| Name: "thingy_id", |
| Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, |
| Values: &stacks.DynamicValueChange{ |
| Old: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), |
| }, |
| New: &stacks.DynamicValue{ |
| Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), |
| }, |
| }, |
| RequiredDuringApply: true, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| for name, test := range tests { |
| t.Run(name, func(t *testing.T) { |
| got, err := test.Receiver.PlannedChangeProto() |
| if len(test.WantErr) > 0 { |
| if diff := cmp.Diff(test.WantErr, err.Error()); diff != "" { |
| t.Errorf("wrong error\n%s", diff) |
| } |
| if got != nil { |
| t.Errorf("unexpected result: %v", got) |
| } |
| return |
| } |
| |
| if err != nil { |
| // All errors this can generate are caused by bugs in Terraform |
| // because we're serializing content that we created, and so |
| // there are no _expected_ error cases. |
| t.Fatal(err) |
| } |
| if diff := cmp.Diff(test.Want, got, protocmp.Transform()); diff != "" { |
| t.Errorf("wrong result\n%s", diff) |
| } |
| }) |
| } |
| } |
| |
| func mustMarshalAnyPb(msg proto.Message) *anypb.Any { |
| var ret anypb.Any |
| err := anypb.MarshalFrom(&ret, msg, proto.MarshalOptions{}) |
| if err != nil { |
| panic(err) |
| } |
| return &ret |
| } |
| |
| func mustMsgPack(t *testing.T, value cty.Value) []byte { |
| data, err := msgpack.Marshal(value, cty.DynamicPseudoType) |
| if err != nil { |
| t.Fatal(err) |
| } |
| return data |
| } |