Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

stacks: handle deferred actions in refresh #34887

Merged
merged 5 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 5 additions & 30 deletions internal/plans/deferring.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,16 @@

package plans

import "github.com/zclconf/go-cty/cty"

type DeferredReason string

const (
// DeferredReasonInvalid is used when the reason for deferring is
// unknown or irrelevant.
DeferredReasonInvalid DeferredReason = "invalid"

// DeferredReasonInstanceCountUnknown is used when the reason for deferring
// is that the count or for_each meta-attribute was unknown.
DeferredReasonInstanceCountUnknown DeferredReason = "instance_count_unknown"

// DeferredReasonResourceConfigUnknown is used when the reason for deferring
// is that the resource configuration was unknown.
DeferredReasonResourceConfigUnknown DeferredReason = "resource_config_unknown"

// DeferredReasonProviderConfigUnknown is used when the reason for deferring
// is that the provider configuration was unknown.
DeferredReasonProviderConfigUnknown DeferredReason = "provider_config_unknown"

// DeferredReasonAbsentPrereq is used when the reason for deferring is that
// a required prerequisite resource was absent.
DeferredReasonAbsentPrereq DeferredReason = "absent_prereq"

// DeferredReasonDeferredPrereq is used when the reason for deferring is
// that a required prerequisite resource was itself deferred.
DeferredReasonDeferredPrereq DeferredReason = "deferred_prereq"
import (
"github.com/hashicorp/terraform/internal/providers"
"github.com/zclconf/go-cty/cty"
)

// DeferredResourceInstanceChangeSrc tracks information about a resource that
// has been deferred for some reason.
type DeferredResourceInstanceChangeSrc struct {
// DeferredReason is the reason why this resource instance was deferred.
DeferredReason DeferredReason
DeferredReason providers.DeferredReason

// ChangeSrc contains any information we have about the deferred change.
// This could be incomplete so must be parsed with care.
Expand All @@ -60,7 +35,7 @@ func (rcs *DeferredResourceInstanceChangeSrc) Decode(ty cty.Type) (*DeferredReso
// has been deferred for some reason.
type DeferredResourceInstanceChange struct {
// DeferredReason is the reason why this resource instance was deferred.
DeferredReason DeferredReason
DeferredReason providers.DeferredReason

// Change contains any information we have about the deferred change. This
// could be incomplete so must be parsed with care.
Expand Down
24 changes: 21 additions & 3 deletions internal/plans/deferring/deferred.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
)

// Deferred keeps track of deferrals that have already happened, to help
Expand Down Expand Up @@ -176,6 +177,22 @@ func (d *Deferred) HaveAnyDeferrals() bool {
len(d.partialExpandedModulesDeferred) != 0)
}

// IsResourceInstanceDeferred returns true if the receiver knows some reason
// why the resource instance with the given address should have its planned
// action deferred for a future plan/apply round.
func (d *Deferred) IsResourceInstanceDeferred(addr addrs.AbsResourceInstance) bool {
if d.externalDependencyDeferred {
return true
}

// Our resource graph describes relationships between the static resource
// configuration blocks, not their dynamic instances, so we need to start
// with the config address that the given instance belongs to.
configAddr := addr.ConfigResource()

return d.resourceInstancesDeferred.Get(configAddr).Has(addr)
}

// ShouldDeferResourceInstanceChanges returns true if the receiver knows some
// reason why the resource instance with the given address should have its
// planned action deferred for a future plan/apply round.
Expand All @@ -195,7 +212,8 @@ func (d *Deferred) HaveAnyDeferrals() bool {
// It's invalid to call this method for an address that was already reported
// as deferred using [Deferred.ReportResourceInstanceDeferred], and so this
// method will panic in that case. Callers should always test whether a resource
// instance action should be deferred _before_ reporting that it has been.
// instance action should be deferred _before_ reporting that it has been by calling
// [Deferred.IsResourceInstanceDeferred].
func (d *Deferred) ShouldDeferResourceInstanceChanges(addr addrs.AbsResourceInstance) bool {
d.mu.Lock()
defer d.mu.Unlock()
Expand Down Expand Up @@ -313,7 +331,7 @@ func (d *Deferred) ReportResourceExpansionDeferred(addr addrs.PartialExpandedRes
panic(fmt.Sprintf("duplicate deferral report for %s", addr))
}
configMap.Put(addr, &plans.DeferredResourceInstanceChange{
DeferredReason: plans.DeferredReasonInstanceCountUnknown,
DeferredReason: providers.DeferredReasonInstanceCountUnknown,
Change: change,
})
}
Expand Down Expand Up @@ -348,7 +366,7 @@ func (d *Deferred) ReportDataSourceExpansionDeferred(addr addrs.PartialExpandedR
// ReportResourceInstanceDeferred records that a fully-expanded resource
// instance has had its planned action deferred to a future round for a reason
// other than its address being only partially-decided.
func (d *Deferred) ReportResourceInstanceDeferred(addr addrs.AbsResourceInstance, reason plans.DeferredReason, change *plans.ResourceInstanceChange) {
func (d *Deferred) ReportResourceInstanceDeferred(addr addrs.AbsResourceInstance, reason providers.DeferredReason, change *plans.ResourceInstanceChange) {
d.mu.Lock()
defer d.mu.Unlock()

Expand Down
3 changes: 2 additions & 1 deletion internal/plans/deferring/deferred_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
)

func TestDeferred_externalDependency(t *testing.T) {
Expand Down Expand Up @@ -90,7 +91,7 @@ func TestDeferred_absResourceInstanceDeferred(t *testing.T) {
})

// Instance A has its Create action deferred for some reason.
deferred.ReportResourceInstanceDeferred(instAAddr, plans.DeferredReasonResourceConfigUnknown, &plans.ResourceInstanceChange{
deferred.ReportResourceInstanceDeferred(instAAddr, providers.DeferredReasonResourceConfigUnknown, &plans.ResourceInstanceChange{
Addr: instAAddr,
Change: plans.Change{
Action: plans.Create,
Expand Down
26 changes: 13 additions & 13 deletions internal/plans/planfile/tfplan.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,20 +464,20 @@ func deferredChangeFromTfplan(dc *planproto.DeferredResourceInstanceChange) (*pl
}, nil
}

func deferredReasonFromProto(reason planproto.DeferredReason) (plans.DeferredReason, error) {
func deferredReasonFromProto(reason planproto.DeferredReason) (providers.DeferredReason, error) {
switch reason {
case planproto.DeferredReason_INSTANCE_COUNT_UNKNOWN:
return plans.DeferredReasonInstanceCountUnknown, nil
return providers.DeferredReasonInstanceCountUnknown, nil
case planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN:
return plans.DeferredReasonResourceConfigUnknown, nil
return providers.DeferredReasonResourceConfigUnknown, nil
case planproto.DeferredReason_PROVIDER_CONFIG_UNKNOWN:
return plans.DeferredReasonProviderConfigUnknown, nil
return providers.DeferredReasonProviderConfigUnknown, nil
case planproto.DeferredReason_ABSENT_PREREQ:
return plans.DeferredReasonAbsentPrereq, nil
return providers.DeferredReasonAbsentPrereq, nil
case planproto.DeferredReason_DEFERRED_PREREQ:
return plans.DeferredReasonDeferredPrereq, nil
return providers.DeferredReasonDeferredPrereq, nil
default:
return plans.DeferredReasonInvalid, fmt.Errorf("invalid deferred reason %s", reason)
return providers.DeferredReasonInvalid, fmt.Errorf("invalid deferred reason %s", reason)
}
}

Expand Down Expand Up @@ -931,17 +931,17 @@ func deferredChangeToTfplan(dc *plans.DeferredResourceInstanceChangeSrc) (*planp
}, nil
}

func deferredReasonToProto(reason plans.DeferredReason) (planproto.DeferredReason, error) {
func deferredReasonToProto(reason providers.DeferredReason) (planproto.DeferredReason, error) {
switch reason {
case plans.DeferredReasonInstanceCountUnknown:
case providers.DeferredReasonInstanceCountUnknown:
return planproto.DeferredReason_INSTANCE_COUNT_UNKNOWN, nil
case plans.DeferredReasonResourceConfigUnknown:
case providers.DeferredReasonResourceConfigUnknown:
return planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN, nil
case plans.DeferredReasonProviderConfigUnknown:
case providers.DeferredReasonProviderConfigUnknown:
return planproto.DeferredReason_PROVIDER_CONFIG_UNKNOWN, nil
case plans.DeferredReasonAbsentPrereq:
case providers.DeferredReasonAbsentPrereq:
return planproto.DeferredReason_ABSENT_PREREQ, nil
case plans.DeferredReasonDeferredPrereq:
case providers.DeferredReasonDeferredPrereq:
return planproto.DeferredReason_DEFERRED_PREREQ, nil
default:
return planproto.DeferredReason_INVALID, fmt.Errorf("invalid deferred reason %s", reason)
Expand Down
3 changes: 2 additions & 1 deletion internal/plans/planfile/tfplan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/internal/lang/globalref"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
)

Expand Down Expand Up @@ -195,7 +196,7 @@ func TestTFPlanRoundTrip(t *testing.T) {
},
DeferredResources: []*plans.DeferredResourceInstanceChangeSrc{
{
DeferredReason: plans.DeferredReasonInstanceCountUnknown,
DeferredReason: providers.DeferredReasonInstanceCountUnknown,
ChangeSrc: &plans.ResourceInstanceChangeSrc{
Addr: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Expand Down
34 changes: 34 additions & 0 deletions internal/plugin/convert/deferred.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package convert

import (
"github.com/hashicorp/terraform/internal/providers"
proto "github.com/hashicorp/terraform/internal/tfplugin5"
)

// ProtoToDeferred translates a proto.Deferred to a providers.Deferred.
func ProtoToDeferred(d *proto.Deferred) *providers.Deferred {
if d == nil {
return nil
}

var reason providers.DeferredReason
switch d.Reason {
case proto.Deferred_UNKNOWN:
reason = providers.DeferredReasonInvalid
case proto.Deferred_RESOURCE_CONFIG_UNKNOWN:
reason = providers.DeferredReasonResourceConfigUnknown
case proto.Deferred_PROVIDER_CONFIG_UNKNOWN:
reason = providers.DeferredReasonProviderConfigUnknown
case proto.Deferred_ABSENT_PREREQ:
reason = providers.DeferredReasonAbsentPrereq
default:
reason = providers.DeferredReasonInvalid
}

return &providers.Deferred{
Reason: reason,
}
}
56 changes: 56 additions & 0 deletions internal/plugin/convert/deferred_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package convert

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/internal/providers"
proto "github.com/hashicorp/terraform/internal/tfplugin5"
)

func TestProtoDeferred(t *testing.T) {
testCases := []struct {
reason proto.Deferred_Reason
expected providers.DeferredReason
}{
{
reason: proto.Deferred_UNKNOWN,
expected: providers.DeferredReasonInvalid,
},
{
reason: proto.Deferred_RESOURCE_CONFIG_UNKNOWN,
expected: providers.DeferredReasonResourceConfigUnknown,
},
{
reason: proto.Deferred_PROVIDER_CONFIG_UNKNOWN,
expected: providers.DeferredReasonProviderConfigUnknown,
},
{
reason: proto.Deferred_ABSENT_PREREQ,
expected: providers.DeferredReasonAbsentPrereq,
},
}

for _, tc := range testCases {
t.Run(fmt.Sprintf("deferred reason %q", tc.reason.String()), func(t *testing.T) {
d := &proto.Deferred{
Reason: tc.reason,
}

deferred := ProtoToDeferred(d)
if deferred.Reason != tc.expected {
t.Fatalf("expected %q, got %q", tc.expected, deferred.Reason)
}
})
}
}

func TestProtoDeferred_Nil(t *testing.T) {
deferred := ProtoToDeferred(nil)
if deferred != nil {
t.Fatalf("expected nil, got %v", deferred)
}
}
8 changes: 5 additions & 3 deletions internal/plugin/grpc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,10 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi
}

protoReq := &proto.ReadResource_Request{
TypeName: r.TypeName,
CurrentState: &proto.DynamicValue{Msgpack: mp},
Private: r.Private,
TypeName: r.TypeName,
CurrentState: &proto.DynamicValue{Msgpack: mp},
Private: r.Private,
DeferralAllowed: r.DeferralAllowed,
}

if metaSchema.Block != nil {
Expand All @@ -418,6 +419,7 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred)
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))

state, err := decodeDynamicValue(protoResp.NewState, resSchema.Block.ImpliedType())
Expand Down
35 changes: 35 additions & 0 deletions internal/plugin/grpc_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,41 @@ func TestGRPCProvider_ReadResource(t *testing.T) {
}
}

func TestGRPCProvider_ReadResource_deferred(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
}

client.EXPECT().ReadResource(
gomock.Any(),
gomock.Any(),
).Return(&proto.ReadResource_Response{
NewState: &proto.DynamicValue{
Msgpack: []byte("\x81\xa4attr\xa3bar"),
},
Deferred: &proto.Deferred{
Reason: proto.Deferred_ABSENT_PREREQ,
},
}, nil)

resp := p.ReadResource(providers.ReadResourceRequest{
TypeName: "resource",
PriorState: cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("foo"),
}),
})

checkDiags(t, resp.Diagnostics)

expectedDeferred := &providers.Deferred{
Reason: providers.DeferredReasonAbsentPrereq,
}
if !cmp.Equal(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty) {
t.Fatal(cmp.Diff(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty))
}
}

func TestGRPCProvider_ReadResourceJSON(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
Expand Down
34 changes: 34 additions & 0 deletions internal/plugin6/convert/deferred.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package convert

import (
"github.com/hashicorp/terraform/internal/providers"
proto "github.com/hashicorp/terraform/internal/tfplugin6"
)

// ProtoToDeferred translates a proto.Deferred to a providers.Deferred.
func ProtoToDeferred(d *proto.Deferred) *providers.Deferred {
if d == nil {
return nil
}

var reason providers.DeferredReason
switch d.Reason {
case proto.Deferred_UNKNOWN:
reason = providers.DeferredReasonInvalid
case proto.Deferred_RESOURCE_CONFIG_UNKNOWN:
reason = providers.DeferredReasonResourceConfigUnknown
case proto.Deferred_PROVIDER_CONFIG_UNKNOWN:
reason = providers.DeferredReasonProviderConfigUnknown
case proto.Deferred_ABSENT_PREREQ:
reason = providers.DeferredReasonAbsentPrereq
default:
reason = providers.DeferredReasonInvalid
}

return &providers.Deferred{
Reason: reason,
}
}
Loading
Loading