Skip to content

Commit cb1f2b6

Browse files
authored
all: Add deferred action testing support (plan checks, version check, and CLI options) (#331)
* sloppy first commit! * add version checks and tests * add changelogs * update terraform-plugin-go * spelling fix * update `terraform-json` * switch to pointer bool value
1 parent 4c2e5cd commit cb1f2b6

19 files changed

+717
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: 'helper/resource: Added `(TestCase).AdditionalCLIOptions` with `AllowDeferral`
3+
option for plan and apply commands.'
4+
time: 2024-05-03T16:17:09.64792-04:00
5+
custom:
6+
Issue: "331"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: FEATURES
2+
body: 'plancheck: Added `ExpectDeferredChange` and `ExpectNoDeferredChanges` checks
3+
for experimental deferred action support.'
4+
time: 2024-05-03T16:15:31.03438-04:00
5+
custom:
6+
Issue: "331"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: FEATURES
2+
body: 'tfversion: Added `SkipIfNotPrerelease` version check for testing experimental
3+
features of prerelease Terraform builds.'
4+
time: 2024-05-03T16:18:02.132794-04:00
5+
custom:
6+
Issue: "331"

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ require (
1515
github.com/hashicorp/logutils v1.0.0
1616
github.com/hashicorp/terraform-exec v0.21.0
1717
github.com/hashicorp/terraform-json v0.22.1
18-
github.com/hashicorp/terraform-plugin-go v0.22.2
18+
github.com/hashicorp/terraform-plugin-go v0.23.0
1919
github.com/hashicorp/terraform-plugin-log v0.9.0
2020
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0
2121
github.com/mitchellh/go-testing-interface v1.14.1
@@ -56,5 +56,5 @@ require (
5656
google.golang.org/appengine v1.6.8 // indirect
5757
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
5858
google.golang.org/grpc v1.63.2 // indirect
59-
google.golang.org/protobuf v1.33.0 // indirect
59+
google.golang.org/protobuf v1.34.0 // indirect
6060
)

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVW
7272
github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg=
7373
github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec=
7474
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
75-
github.com/hashicorp/terraform-plugin-go v0.22.2 h1:5o8uveu6eZUf5J7xGPV0eY0TPXg3qpmwX9sce03Bxnc=
76-
github.com/hashicorp/terraform-plugin-go v0.22.2/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM=
75+
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
76+
github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
7777
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
7878
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
7979
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 h1:qHprzXy/As0rxedphECBEQAh3R4yp6pKksKHcqZx5G8=
@@ -200,8 +200,8 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
200200
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
201201
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
202202
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
203-
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
204-
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
203+
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
204+
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
205205
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
206206
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
207207
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package resource
5+
6+
// AdditionalCLIOptions allows an intentionally limited set of options to be passed
7+
// to the Terraform CLI when executing test steps.
8+
type AdditionalCLIOptions struct {
9+
// Apply represents options to be passed to the `terraform apply` command.
10+
Apply ApplyOptions
11+
12+
// Plan represents options to be passed to the `terraform plan` command.
13+
Plan PlanOptions
14+
}
15+
16+
// ApplyOptions represents options to be passed to the `terraform apply` command.
17+
type ApplyOptions struct {
18+
// AllowDeferral will pass the experimental `-allow-deferral` flag to the apply command.
19+
AllowDeferral bool
20+
}
21+
22+
// PlanOptions represents options to be passed to the `terraform plan` command.
23+
type PlanOptions struct {
24+
// AllowDeferral will pass the experimental `-allow-deferral` flag to the plan command.
25+
AllowDeferral bool
26+
}

helper/resource/testing.go

+4
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,10 @@ type TestCase struct {
441441
// set to "1", to persist any working directory files. Otherwise, this directory is
442442
// automatically cleaned up at the end of the TestCase.
443443
WorkingDir string
444+
445+
// AdditionalCLIOptions allows an intentionally limited set of options to be passed
446+
// to the Terraform CLI when executing test steps.
447+
AdditionalCLIOptions *AdditionalCLIOptions
444448
}
445449

446450
// ExternalProvider holds information about third-party providers that should

helper/resource/testing_new_config.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
107107
if step.Destroy {
108108
opts = append(opts, tfexec.Destroy(true))
109109
}
110+
111+
if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral {
112+
opts = append(opts, tfexec.AllowDeferral(true))
113+
}
114+
110115
return wd.CreatePlan(ctx, opts...)
111116
}, wd, providers)
112117
if err != nil {
@@ -168,7 +173,13 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
168173

169174
// Apply the diff, creating real resources
170175
err = runProviderCommand(ctx, t, func() error {
171-
return wd.Apply(ctx)
176+
var opts []tfexec.ApplyOption
177+
178+
if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Apply.AllowDeferral {
179+
opts = append(opts, tfexec.AllowDeferral(true))
180+
}
181+
182+
return wd.Apply(ctx, opts...)
172183
}, wd, providers)
173184
if err != nil {
174185
if step.Destroy {
@@ -238,6 +249,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
238249
if step.Destroy {
239250
opts = append(opts, tfexec.Destroy(true))
240251
}
252+
253+
if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral {
254+
opts = append(opts, tfexec.AllowDeferral(true))
255+
}
256+
241257
return wd.CreatePlan(ctx, opts...)
242258
}, wd, providers)
243259
if err != nil {
@@ -302,6 +318,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
302318
opts = append(opts, tfexec.Refresh(false))
303319
}
304320
}
321+
322+
if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral {
323+
opts = append(opts, tfexec.AllowDeferral(true))
324+
}
325+
305326
return wd.CreatePlan(ctx, opts...)
306327
}, wd, providers)
307328
if err != nil {

internal/plugintest/working_dir.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,9 @@ func (wd *WorkingDir) CreatePlan(ctx context.Context, opts ...tfexec.PlanOption)
257257
// successfully and the saved plan has not been cleared in the meantime then
258258
// this will apply the saved plan. Otherwise, it will implicitly create a new
259259
// plan and apply it.
260-
func (wd *WorkingDir) Apply(ctx context.Context) error {
260+
func (wd *WorkingDir) Apply(ctx context.Context, opts ...tfexec.ApplyOption) error {
261261
args := []tfexec.ApplyOption{tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false)}
262+
args = append(args, opts...)
262263
if wd.HasSavedPlan() {
263264
args = append(args, tfexec.DirOrPlan(PlanFileName))
264265
}

internal/testing/testsdk/providerserver/providerserver.go

+13
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ type ProviderServer struct {
5858
Provider provider.Provider
5959
}
6060

61+
func (s ProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) {
62+
return &tfprotov6.CallFunctionResponse{}, nil
63+
}
64+
65+
func (s ProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) {
66+
return &tfprotov6.GetFunctionsResponse{}, nil
67+
}
68+
69+
func (s ProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) {
70+
return &tfprotov6.MoveResourceStateResponse{}, nil
71+
}
72+
6173
func (s ProviderServer) GetMetadata(ctx context.Context, request *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) {
6274
resp := &tfprotov6.GetMetadataResponse{
6375
ServerCapabilities: &tfprotov6.ServerCapabilities{
@@ -448,6 +460,7 @@ func (s ProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.P
448460

449461
resp.Diagnostics = planResp.Diagnostics
450462
resp.RequiresReplace = planResp.RequiresReplace
463+
resp.Deferred = planResp.Deferred
451464

452465
if len(resp.Diagnostics) > 0 {
453466
return resp, nil

internal/testing/testsdk/providerserver/providerserver_protov5.go

+14
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ type Protov5ProviderServer struct {
3636
Provider provider.Protov5Provider
3737
}
3838

39+
// CallFunction implements tfprotov5.ProviderServer.
40+
func (s Protov5ProviderServer) CallFunction(ctx context.Context, req *tfprotov5.CallFunctionRequest) (*tfprotov5.CallFunctionResponse, error) {
41+
return &tfprotov5.CallFunctionResponse{}, nil
42+
}
43+
44+
// GetFunctions implements tfprotov5.ProviderServer.
45+
func (s Protov5ProviderServer) GetFunctions(ctx context.Context, req *tfprotov5.GetFunctionsRequest) (*tfprotov5.GetFunctionsResponse, error) {
46+
return &tfprotov5.GetFunctionsResponse{}, nil
47+
}
48+
49+
func (s Protov5ProviderServer) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) {
50+
return &tfprotov5.MoveResourceStateResponse{}, nil
51+
}
52+
3953
func (s Protov5ProviderServer) GetMetadata(ctx context.Context, request *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) {
4054
return &tfprotov5.GetMetadataResponse{}, nil
4155
}

internal/testing/testsdk/resource/resource.go

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type PlanChangeRequest struct {
5555
}
5656

5757
type PlanChangeResponse struct {
58+
Deferred *tfprotov6.Deferred
5859
Diagnostics []*tfprotov6.Diagnostic
5960
PlannedState tftypes.Value
6061
RequiresReplace []*tftypes.AttributePath

plancheck/deferred_reason.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package plancheck
5+
6+
// DeferredReason is a string stored in the plan file which indicates why Terraform
7+
// is deferring a change for a resource.
8+
type DeferredReason string
9+
10+
const (
11+
// DeferredReasonResourceConfigUnknown is used to indicate that the resource configuration
12+
// is partially unknown and the real values need to be known before the change can be planned.
13+
DeferredReasonResourceConfigUnknown DeferredReason = "resource_config_unknown"
14+
15+
// DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration
16+
// is partially unknown and the real values need to be known before the change can be planned.
17+
DeferredReasonProviderConfigUnknown DeferredReason = "provider_config_unknown"
18+
19+
// DeferredReasonAbsentPrereq is used to indicate that a hard dependency has not been satisfied.
20+
DeferredReasonAbsentPrereq DeferredReason = "absent_prereq"
21+
)

plancheck/expect_deferred_change.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package plancheck
5+
6+
import (
7+
"context"
8+
"fmt"
9+
)
10+
11+
var _ PlanCheck = expectDeferredChange{}
12+
13+
type expectDeferredChange struct {
14+
resourceAddress string
15+
reason DeferredReason
16+
}
17+
18+
// CheckPlan implements the plan check logic.
19+
func (e expectDeferredChange) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) {
20+
foundResource := false
21+
22+
for _, dc := range req.Plan.DeferredChanges {
23+
if dc.ResourceChange == nil || e.resourceAddress != dc.ResourceChange.Address {
24+
continue
25+
}
26+
27+
if e.reason != DeferredReason(dc.Reason) {
28+
resp.Error = fmt.Errorf("'%s' - expected %q, got deferred reason: %q", dc.ResourceChange.Address, e.reason, dc.Reason)
29+
return
30+
}
31+
32+
foundResource = true
33+
break
34+
}
35+
36+
if !foundResource {
37+
resp.Error = fmt.Errorf("%s - No deferred changes found for resource", e.resourceAddress)
38+
return
39+
}
40+
}
41+
42+
// ExpectDeferredChange returns a plan check that asserts that a given resource will have a
43+
// deferred change in the plan with the given reason.
44+
func ExpectDeferredChange(resourceAddress string, reason DeferredReason) PlanCheck {
45+
return expectDeferredChange{
46+
resourceAddress: resourceAddress,
47+
reason: reason,
48+
}
49+
}

0 commit comments

Comments
 (0)