You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform/internal/stacks/stackplan/planned_change_test.go

818 lines
28 KiB

// 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"
"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/plans"
"github.com/hashicorp/terraform/internal/plans/planproto"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
"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 *terraform1.PlannedChange
}{
"header": {
Receiver: &PlannedChangeHeader{
TerraformVersion: version.Must(version.NewSemver("1.2.3-beta4")),
},
Want: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanHeader{
TerraformVersion: "1.2.3-beta4",
}),
},
},
},
"applyable true": {
Receiver: &PlannedChangeApplyable{
Applyable: true,
},
Want: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanApplyable{
Applyable: true,
}),
},
Descriptions: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_PlanApplyable{
PlanApplyable: true,
},
},
},
},
},
"applyable false": {
Receiver: &PlannedChangeApplyable{
Applyable: false,
},
Want: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanApplyable{
// false is the default
}),
},
Descriptions: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.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: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{
ComponentInstanceAddr: "component.foo",
PlanTimestamp: "2017-03-27T10:00:00-08:00",
PlannedAction: planproto.Action_CREATE,
}),
},
Descriptions: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ComponentInstancePlanned{
ComponentInstancePlanned: &terraform1.PlannedChange_ComponentInstance{
Addr: &terraform1.ComponentInstanceInStackAddr{
ComponentAddr: "component.foo",
ComponentInstanceAddr: "component.foo",
},
Actions: []terraform1.ChangeType{terraform1.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: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{
ComponentInstanceAddr: `component.foo["bar"]`,
PlanTimestamp: "2017-03-27T10:00:00-08:00",
}),
},
Descriptions: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ComponentInstancePlanned{
ComponentInstancePlanned: &terraform1.PlannedChange_ComponentInstance{
Actions: []terraform1.ChangeType{terraform1.ChangeType_NOOP},
Addr: &terraform1.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: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{
ComponentInstanceAddr: `stack.a["boop"].component.foo`,
PlannedAction: planproto.Action_DELETE,
}),
},
Descriptions: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ComponentInstancePlanned{
ComponentInstancePlanned: &terraform1.PlannedChange_ComponentInstance{
Addr: &terraform1.ComponentInstanceInStackAddr{
ComponentAddr: "stack.a.component.foo",
ComponentInstanceAddr: `stack.a["boop"].component.foo`,
},
Actions: []terraform1.ChangeType{terraform1.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: &terraform1.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: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ResourceInstanceDeferred{
ResourceInstanceDeferred: &terraform1.PlannedChange_ResourceInstanceDeferred{
ResourceInstance: &terraform1.PlannedChange_ResourceInstance{
Addr: &terraform1.ResourceInstanceObjectInStackAddr{
ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`,
ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`,
DeposedKey: "aaaaaaaa",
},
ResourceMode: terraform1.ResourceMode_MANAGED,
ResourceType: "thingy",
ProviderAddr: "example.com/thingers/thingy",
Actions: []terraform1.ChangeType{terraform1.ChangeType_CREATE},
Values: &terraform1.DynamicValueChange{
Old: &terraform1.DynamicValue{
Msgpack: []byte{'\xc0'}, // null
},
New: &terraform1.DynamicValue{
Msgpack: []byte{'\x80'}, // zero-length mapping
},
},
},
Deferred: &terraform1.Deferred{
Reason: terraform1.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: &terraform1.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: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ResourceInstancePlanned{
ResourceInstancePlanned: &terraform1.PlannedChange_ResourceInstance{
Addr: &terraform1.ResourceInstanceObjectInStackAddr{
ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`,
ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`,
DeposedKey: "aaaaaaaa",
},
ResourceMode: terraform1.ResourceMode_MANAGED,
ResourceType: "thingy",
ProviderAddr: "example.com/thingers/thingy",
Actions: []terraform1.ChangeType{terraform1.ChangeType_CREATE},
Values: &terraform1.DynamicValueChange{
Old: &terraform1.DynamicValue{
Msgpack: []byte{'\xc0'}, // null
},
New: &terraform1.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: &terraform1.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: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ResourceInstancePlanned{
ResourceInstancePlanned: &terraform1.PlannedChange_ResourceInstance{
Addr: &terraform1.ResourceInstanceObjectInStackAddr{
ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`,
ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`,
DeposedKey: "aaaaaaaa",
},
ResourceMode: terraform1.ResourceMode_MANAGED,
ResourceType: "thingy",
ProviderAddr: "example.com/thingers/thingy",
Actions: []terraform1.ChangeType{terraform1.ChangeType_DELETE, terraform1.ChangeType_CREATE},
Values: &terraform1.DynamicValueChange{
Old: &terraform1.DynamicValue{
Msgpack: []byte("\x81\xa3foo\xa3bar"),
},
New: &terraform1.DynamicValue{
Msgpack: []byte("\x81\xa3foo\xa3baz"),
},
},
ReplacePaths: []*terraform1.AttributePath{
{
Steps: []*terraform1.AttributePath_Step{
{
Selector: &terraform1.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: &terraform1.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: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ResourceInstancePlanned{
ResourceInstancePlanned: &terraform1.PlannedChange_ResourceInstance{
Addr: &terraform1.ResourceInstanceObjectInStackAddr{
ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`,
ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`,
},
ResourceMode: terraform1.ResourceMode_MANAGED,
ResourceType: "thingy",
ProviderAddr: "example.com/thingers/thingy",
Actions: []terraform1.ChangeType{terraform1.ChangeType_NOOP},
Values: &terraform1.DynamicValueChange{
Old: &terraform1.DynamicValue{
Msgpack: []byte{'\x80'}, // zero-length mapping
},
New: &terraform1.DynamicValue{
Msgpack: []byte{'\x80'}, // zero-length mapping
},
},
Imported: &terraform1.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: &terraform1.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: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_ResourceInstancePlanned{
ResourceInstancePlanned: &terraform1.PlannedChange_ResourceInstance{
Addr: &terraform1.ResourceInstanceObjectInStackAddr{
ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`,
ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`,
},
ResourceMode: terraform1.ResourceMode_MANAGED,
ResourceType: "thingy",
ProviderAddr: "example.com/thingers/thingy",
Actions: []terraform1.ChangeType{terraform1.ChangeType_NOOP},
Values: &terraform1.DynamicValueChange{
Old: &terraform1.DynamicValue{
Msgpack: []byte{'\x80'}, // zero-length mapping
},
New: &terraform1.DynamicValue{
Msgpack: []byte{'\x80'}, // zero-length mapping
},
},
Moved: &terraform1.PlannedChange_ResourceInstance_Moved{
PrevAddr: &terraform1.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".
OldValue: emptyObjectForPlan,
NewValue: emptyObjectForPlan,
},
Want: &terraform1.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: []*terraform1.PlannedChange_ChangeDescription{
{
Description: &terraform1.PlannedChange_ChangeDescription_OutputValuePlanned{
OutputValuePlanned: &terraform1.PlannedChange_OutputValue{
Name: "thingy_id",
Actions: []terraform1.ChangeType{terraform1.ChangeType_UPDATE},
Values: &terraform1.DynamicValueChange{
Old: &terraform1.DynamicValue{
Msgpack: []byte{'\x80'}, // zero-length mapping
},
New: &terraform1.DynamicValue{
Msgpack: []byte{'\x80'}, // zero-length mapping
},
},
},
},
},
},
},
},
"root input variable": {
Receiver: &PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "thingy_id"},
Value: cty.StringVal("boop"),
},
Want: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{
Name: "thingy_id",
Value: &planproto.DynamicValue{
Msgpack: []byte("\x92\xc4\b\"string\"\xa4boop"),
},
}),
},
},
},
"root input variable that must be re-supplied during apply": {
Receiver: &PlannedChangeRootInputValue{
Addr: stackaddrs.InputVariable{Name: "thingy_id"},
RequiredOnApply: true,
// No value in this case: the value must be re-supplied during
// apply specifically so that we can avoid the need to store it.
},
Want: &terraform1.PlannedChange{
Raw: []*anypb.Any{
mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{
Name: "thingy_id",
RequiredOnApply: true,
}),
},
Descriptions: []*terraform1.PlannedChange_ChangeDescription{
&terraform1.PlannedChange_ChangeDescription{
Description: &terraform1.PlannedChange_ChangeDescription_ApplyTimeInputVariable{
ApplyTimeInputVariable: &terraform1.PlannedChange_InputVariableDuringApply{
Name: "thingy_id",
},
},
},
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, err := test.Receiver.PlannedChangeProto()
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
}