Skip to content

Commit 02c429c

Browse files
authored
all: Add automatic deferred action support for unknown provider configuration (#1335)
* update plugin-go * quick implementation of automatic deferral (no opt-in currently) * add opt-in logic with ResourceBehavior * update naming on exported structs/fields * add data source tests * add readresource tests * refactor planresourcechange tests * remove configure from tests * add new PlanResourceChange RPC tests * update import logic + add tests for ImportResourceState * update sdkv2 to match new protocol changes + pass deferral allowed to configureprovider * update terraform-plugin-go * update plugin-go * name changes + doc updates + remove duplicate diags * add logging * error message update * pkg doc updates * add changelogs * add copywrite header * go mod tidy :) * replace TODO comment on import * replace if check for deferralAllowed * replace logging key with a constant * use the proper error returning method for test assertions * add experimental verbiage * DeferredResponse -> Deferred
1 parent b76c8ef commit 02c429c

File tree

9 files changed

+2452
-192
lines changed

9 files changed

+2452
-192
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: FEATURES
2+
body: 'helper/schema: Added `(Provider).ConfigureProvider` function for configuring
3+
providers that support additional features, such as deferred actions.'
4+
time: 2024-05-06T15:20:18.393505-04:00
5+
custom:
6+
Issue: "1335"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: FEATURES
2+
body: 'helper/schema: Added `(Resource).ResourceBehavior` to allow additional control
3+
over deferred action behavior during plan modification.'
4+
time: 2024-05-06T15:21:35.304825-04:00
5+
custom:
6+
Issue: "1335"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: NOTES
2+
body: This release contains support for deferred actions, which is an experimental
3+
feature only available in prerelease builds of Terraform 1.9 and later. This functionality
4+
is subject to change and is not protected by version compatibility guarantees.
5+
time: 2024-05-09T13:49:45.38523-04:00
6+
custom:
7+
Issue: "1335"

helper/schema/deferred.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package schema
5+
6+
// MAINTAINER NOTE: Only PROVIDER_CONFIG_UNKNOWN (enum value 2 in the plugin-protocol) is relevant
7+
// for SDKv2. Since (Deferred).Reason is mapped directly to the plugin-protocol,
8+
// the other enum values are intentionally omitted here.
9+
const (
10+
// DeferredReasonUnknown is used to indicate an invalid `DeferredReason`.
11+
// Provider developers should not use it.
12+
DeferredReasonUnknown DeferredReason = 0
13+
14+
// DeferredReasonProviderConfigUnknown represents a deferred reason caused
15+
// by unknown provider configuration.
16+
DeferredReasonProviderConfigUnknown DeferredReason = 2
17+
)
18+
19+
// Deferred is used to indicate to Terraform that a resource or data source is not able
20+
// to be applied yet and should be skipped (deferred). After completing an apply that has deferred actions,
21+
// the practitioner can then execute additional plan and apply “rounds” to eventually reach convergence
22+
// where there are no remaining deferred actions.
23+
//
24+
// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject
25+
// to change or break without warning. It is not protected by version compatibility guarantees.
26+
type Deferred struct {
27+
// Reason represents the deferred reason.
28+
Reason DeferredReason
29+
}
30+
31+
// DeferredReason represents different reasons for deferring a change.
32+
//
33+
// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject
34+
// to change or break without warning. It is not protected by version compatibility guarantees.
35+
type DeferredReason int32
36+
37+
func (d DeferredReason) String() string {
38+
switch d {
39+
case 0:
40+
return "Unknown"
41+
case 2:
42+
return "Provider Config Unknown"
43+
}
44+
return "Unknown"
45+
}

helper/schema/grpc_provider.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,12 +607,37 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto
607607
// request scoped contexts, however this is a large undertaking for very large providers.
608608
ctxHack := context.WithValue(ctx, StopContextKey, s.StopContext(context.Background()))
609609

610+
// NOTE: This is a hack to pass the deferral_allowed field from the Terraform client to the
611+
// underlying (provider).Configure function, which cannot be changed because the function
612+
// signature is public. (╯°□°)╯︵ ┻━┻
613+
s.provider.deferralAllowed = configureDeferralAllowed(req.ClientCapabilities)
614+
610615
logging.HelperSchemaTrace(ctx, "Calling downstream")
611616
diags := s.provider.Configure(ctxHack, config)
612617
logging.HelperSchemaTrace(ctx, "Called downstream")
613618

614619
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags)
615620

621+
if s.provider.providerDeferred != nil {
622+
// Check if a deferred response was incorrectly set on the provider. This would cause an error during later RPCs.
623+
if !s.provider.deferralAllowed {
624+
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
625+
Severity: tfprotov5.DiagnosticSeverityError,
626+
Summary: "Invalid Deferred Provider Response",
627+
Detail: "Provider configured a deferred response for all resources and data sources but the Terraform request " +
628+
"did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers.",
629+
})
630+
} else {
631+
logging.HelperSchemaDebug(
632+
ctx,
633+
"Provider has configured a deferred response, all associated resources and data sources will automatically return a deferred response.",
634+
map[string]interface{}{
635+
logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(),
636+
},
637+
)
638+
}
639+
}
640+
616641
return resp, nil
617642
}
618643

@@ -632,6 +657,22 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
632657
}
633658
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
634659

660+
if s.provider.providerDeferred != nil {
661+
logging.HelperSchemaDebug(
662+
ctx,
663+
"Provider has deferred response configured, automatically returning deferred response.",
664+
map[string]interface{}{
665+
logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(),
666+
},
667+
)
668+
669+
resp.NewState = req.CurrentState
670+
resp.Deferred = &tfprotov5.Deferred{
671+
Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason),
672+
}
673+
return resp, nil
674+
}
675+
635676
stateVal, err := msgpack.Unmarshal(req.CurrentState.MsgPack, schemaBlock.ImpliedType())
636677
if err != nil {
637678
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
@@ -731,6 +772,25 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
731772
resp.UnsafeToUseLegacyTypeSystem = true
732773
}
733774

775+
// Provider deferred response is present and the resource hasn't opted-in to CustomizeDiff being called, return early
776+
// with proposed new state as a best effort for PlannedState.
777+
if s.provider.providerDeferred != nil && !res.ResourceBehavior.ProviderDeferred.EnablePlanModification {
778+
logging.HelperSchemaDebug(
779+
ctx,
780+
"Provider has deferred response configured, automatically returning deferred response.",
781+
map[string]interface{}{
782+
logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(),
783+
},
784+
)
785+
786+
resp.PlannedState = req.ProposedNewState
787+
resp.PlannedPrivate = req.PriorPrivate
788+
resp.Deferred = &tfprotov5.Deferred{
789+
Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason),
790+
}
791+
return resp, nil
792+
}
793+
734794
priorStateVal, err := msgpack.Unmarshal(req.PriorState.MsgPack, schemaBlock.ImpliedType())
735795
if err != nil {
736796
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
@@ -951,6 +1011,21 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
9511011
resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p))
9521012
}
9531013

1014+
// Provider deferred response is present, add the deferred response alongside the provider-modified plan
1015+
if s.provider.providerDeferred != nil {
1016+
logging.HelperSchemaDebug(
1017+
ctx,
1018+
"Provider has deferred response configured, returning deferred response with modified plan.",
1019+
map[string]interface{}{
1020+
logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(),
1021+
},
1022+
)
1023+
1024+
resp.Deferred = &tfprotov5.Deferred{
1025+
Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason),
1026+
}
1027+
}
1028+
9541029
return resp, nil
9551030
}
9561031

@@ -1145,6 +1220,48 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro
11451220
Type: req.TypeName,
11461221
}
11471222

1223+
if s.provider.providerDeferred != nil {
1224+
logging.HelperSchemaDebug(
1225+
ctx,
1226+
"Provider has deferred response configured, automatically returning deferred response.",
1227+
map[string]interface{}{
1228+
logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(),
1229+
},
1230+
)
1231+
1232+
// The logic for ensuring the resource type is supported by this provider is inside of (provider).ImportState
1233+
// We need to check to ensure the resource type is supported before using the schema
1234+
_, ok := s.provider.ResourcesMap[req.TypeName]
1235+
if !ok {
1236+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName))
1237+
return resp, nil
1238+
}
1239+
1240+
// Since we are automatically deferring, send back an unknown value for the imported object
1241+
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
1242+
unknownVal := cty.UnknownVal(schemaBlock.ImpliedType())
1243+
unknownStateMp, err := msgpack.Marshal(unknownVal, schemaBlock.ImpliedType())
1244+
if err != nil {
1245+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
1246+
return resp, nil
1247+
}
1248+
1249+
resp.ImportedResources = []*tfprotov5.ImportedResource{
1250+
{
1251+
TypeName: req.TypeName,
1252+
State: &tfprotov5.DynamicValue{
1253+
MsgPack: unknownStateMp,
1254+
},
1255+
},
1256+
}
1257+
1258+
resp.Deferred = &tfprotov5.Deferred{
1259+
Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason),
1260+
}
1261+
1262+
return resp, nil
1263+
}
1264+
11481265
newInstanceStates, err := s.provider.ImportState(ctx, info, req.ID)
11491266
if err != nil {
11501267
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
@@ -1254,6 +1371,32 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5.
12541371

12551372
schemaBlock := s.getDatasourceSchemaBlock(req.TypeName)
12561373

1374+
if s.provider.providerDeferred != nil {
1375+
logging.HelperSchemaDebug(
1376+
ctx,
1377+
"Provider has deferred response configured, automatically returning deferred response.",
1378+
map[string]interface{}{
1379+
logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(),
1380+
},
1381+
)
1382+
1383+
// Send an unknown value for the data source
1384+
unknownVal := cty.UnknownVal(schemaBlock.ImpliedType())
1385+
unknownStateMp, err := msgpack.Marshal(unknownVal, schemaBlock.ImpliedType())
1386+
if err != nil {
1387+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
1388+
return resp, nil
1389+
}
1390+
1391+
resp.State = &tfprotov5.DynamicValue{
1392+
MsgPack: unknownStateMp,
1393+
}
1394+
resp.Deferred = &tfprotov5.Deferred{
1395+
Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason),
1396+
}
1397+
return resp, nil
1398+
}
1399+
12571400
configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType())
12581401
if err != nil {
12591402
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
@@ -1674,3 +1817,14 @@ func validateConfigNulls(ctx context.Context, v cty.Value, path cty.Path) []*tfp
16741817

16751818
return diags
16761819
}
1820+
1821+
// Helper function that check a ConfigureProviderClientCapabilities struct to determine if a deferred response can be
1822+
// returned to the Terraform client. If no ConfigureProviderClientCapabilities have been passed from the client, then false
1823+
// is returned.
1824+
func configureDeferralAllowed(in *tfprotov5.ConfigureProviderClientCapabilities) bool {
1825+
if in == nil {
1826+
return false
1827+
}
1828+
1829+
return in.DeferralAllowed
1830+
}

0 commit comments

Comments
 (0)