From 8048f680fbddb871c83aede21a70f9ca5e897188 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 26 Apr 2024 14:53:21 -0400 Subject: [PATCH 1/7] sloppy first commit! --- go.mod | 4 +-- go.sum | 8 ++--- helper/resource/testing.go | 12 +++++++ helper/resource/testing_new_config.go | 7 +++- internal/plugintest/working_dir.go | 4 ++- plancheck/deferred_reason.go | 16 +++++++++ plancheck/expect_deferred_reason.go | 48 +++++++++++++++++++++++++ plancheck/expect_no_deferred_changes.go | 38 ++++++++++++++++++++ 8 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 plancheck/deferred_reason.go create mode 100644 plancheck/expect_deferred_reason.go create mode 100644 plancheck/expect_no_deferred_changes.go diff --git a/go.mod b/go.mod index 551e1024b..d7935aae8 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,8 @@ require ( github.com/hashicorp/hc-install v0.6.4 github.com/hashicorp/hcl/v2 v2.20.1 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.20.0 - github.com/hashicorp/terraform-json v0.21.0 + github.com/hashicorp/terraform-exec v0.20.1-0.20240426181105-399b96b1d2ec + github.com/hashicorp/terraform-json v0.21.1-0.20240426182256-aa6b5af6a6ac github.com/hashicorp/terraform-plugin-go v0.22.2 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 diff --git a/go.sum b/go.sum index d3265fbb2..1aefcc738 100644 --- a/go.sum +++ b/go.sum @@ -68,10 +68,10 @@ github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdx github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo= -github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= -github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= -github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= +github.com/hashicorp/terraform-exec v0.20.1-0.20240426181105-399b96b1d2ec h1:zCouqvr8Yw0uyAq+UekEcREozRnwZaUVet9uCajcCbk= +github.com/hashicorp/terraform-exec v0.20.1-0.20240426181105-399b96b1d2ec/go.mod h1:rHqaL9Y7oPlRDZffl2xr/UkSrIhKL2l9G5P81Sza7Ts= +github.com/hashicorp/terraform-json v0.21.1-0.20240426182256-aa6b5af6a6ac h1:V6nEe62yvDbP55vM9Oc1IzqB6JscgkBM6JjjtZG5HuA= +github.com/hashicorp/terraform-json v0.21.1-0.20240426182256-aa6b5af6a6ac/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= github.com/hashicorp/terraform-plugin-go v0.22.2 h1:5o8uveu6eZUf5J7xGPV0eY0TPXg3qpmwX9sce03Bxnc= github.com/hashicorp/terraform-plugin-go v0.22.2/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= diff --git a/helper/resource/testing.go b/helper/resource/testing.go index f98abda39..b35cee820 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -17,6 +17,7 @@ import ( "github.com/mitchellh/go-testing-interface" + "github.com/hashicorp/terraform-exec/tfexec" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -776,6 +777,17 @@ type TestStep struct { // for performing import testing where the prior TestStep configuration // contained a provider outside the one under test. ExternalProviders map[string]ExternalProvider + + // TODO: doc + AdditionalCLIFlags AdditionalCLIFlags +} + +// TODO: doc +type AdditionalCLIFlags struct { + // TODO: doc + Apply []tfexec.ApplyOption + // TODO: doc + Plan []tfexec.PlanOption } // ConfigPlanChecks defines the different points in a Config TestStep when plan checks can be run. diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index 9747eaf6e..6dc9cdcc7 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -107,6 +107,8 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint if step.Destroy { opts = append(opts, tfexec.Destroy(true)) } + + opts = append(opts, step.AdditionalCLIFlags.Plan...) return wd.CreatePlan(ctx, opts...) }, wd, providers) if err != nil { @@ -168,7 +170,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // Apply the diff, creating real resources err = runProviderCommand(ctx, t, func() error { - return wd.Apply(ctx) + return wd.Apply(ctx, step.AdditionalCLIFlags.Apply...) }, wd, providers) if err != nil { if step.Destroy { @@ -238,6 +240,8 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint if step.Destroy { opts = append(opts, tfexec.Destroy(true)) } + + opts = append(opts, step.AdditionalCLIFlags.Plan...) return wd.CreatePlan(ctx, opts...) }, wd, providers) if err != nil { @@ -302,6 +306,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint opts = append(opts, tfexec.Refresh(false)) } } + opts = append(opts, step.AdditionalCLIFlags.Plan...) return wd.CreatePlan(ctx, opts...) }, wd, providers) if err != nil { diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 2c8888d72..2c762a3a3 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -257,8 +257,10 @@ func (wd *WorkingDir) CreatePlan(ctx context.Context, opts ...tfexec.PlanOption) // successfully and the saved plan has not been cleared in the meantime then // this will apply the saved plan. Otherwise, it will implicitly create a new // plan and apply it. -func (wd *WorkingDir) Apply(ctx context.Context) error { +func (wd *WorkingDir) Apply(ctx context.Context, opts ...tfexec.ApplyOption) error { + // TODO: dedupe? args := []tfexec.ApplyOption{tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false)} + args = append(args, opts...) if wd.HasSavedPlan() { args = append(args, tfexec.DirOrPlan(PlanFileName)) } diff --git a/plancheck/deferred_reason.go b/plancheck/deferred_reason.go new file mode 100644 index 000000000..86e8e362b --- /dev/null +++ b/plancheck/deferred_reason.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +// TODO: doc +type DeferredReason string + +const ( + // TODO: doc + DeferredReasonResourceConfigUnknown DeferredReason = "resource_config_unknown" + // TODO: doc + DeferredReasonProviderConfigUnknown DeferredReason = "provider_config_unknown" + // TODO: doc + DeferredReasonAbsentPrereq DeferredReason = "absent_prereq" +) diff --git a/plancheck/expect_deferred_reason.go b/plancheck/expect_deferred_reason.go new file mode 100644 index 000000000..b222e27b9 --- /dev/null +++ b/plancheck/expect_deferred_reason.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "fmt" +) + +var _ PlanCheck = expectDeferredReason{} + +type expectDeferredReason struct { + resourceAddress string + reason DeferredReason +} + +// CheckPlan implements the plan check logic. +func (e expectDeferredReason) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + foundResource := false + + for _, dc := range req.Plan.DeferredChanges { + if dc.ResourceChange == nil || e.resourceAddress != dc.ResourceChange.Address { + continue + } + + if e.reason != DeferredReason(dc.Reason) { + resp.Error = fmt.Errorf("'%s' - expected %s, got deferred reason: %s", dc.ResourceChange.Address, e.reason, dc.Reason) + return + } + + foundResource = true + break + } + + if !foundResource { + resp.Error = fmt.Errorf("%s - Resource not found in plan DeferredChanges", e.resourceAddress) + return + } +} + +// TODO: doc +func ExpectDeferredReason(resourceAddress string, reason DeferredReason) PlanCheck { + return expectDeferredReason{ + resourceAddress: resourceAddress, + reason: reason, + } +} diff --git a/plancheck/expect_no_deferred_changes.go b/plancheck/expect_no_deferred_changes.go new file mode 100644 index 000000000..fceb4946e --- /dev/null +++ b/plancheck/expect_no_deferred_changes.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck + +import ( + "context" + "errors" + "fmt" +) + +var _ PlanCheck = expectNoDeferredChanges{} + +type expectNoDeferredChanges struct{} + +// CheckPlan implements the plan check logic. +func (e expectNoDeferredChanges) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { + if len(req.Plan.DeferredChanges) == 0 { + return + } + + var result []error + for _, deferred := range req.Plan.DeferredChanges { + resourceAddress := "unknown" + if deferred.ResourceChange != nil { + resourceAddress = deferred.ResourceChange.Address + } + + result = append(result, fmt.Errorf("expected no deferred changes, but resource %q is deferred with reason: %q", resourceAddress, deferred.Reason)) + } + + resp.Error = errors.Join(result...) +} + +// TODO: doc +func ExpectNoDeferredChanges() PlanCheck { + return expectNoDeferredChanges{} +} From 6d0a2e26f9f3f50f911da1a584272ed770833482 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 3 May 2024 16:10:43 -0400 Subject: [PATCH 2/7] add version checks and tests --- go.mod | 8 +- go.sum | 16 +- helper/resource/additional_cli_options.go | 26 +++ helper/resource/testing.go | 16 +- helper/resource/testing_new_config.go | 24 ++- internal/plugintest/working_dir.go | 1 - .../testsdk/providerserver/providerserver.go | 13 ++ .../providerserver/providerserver_protov5.go | 14 ++ internal/testing/testsdk/resource/resource.go | 1 + plancheck/deferred_reason.go | 13 +- ...ed_reason.go => expect_deferred_change.go} | 17 +- plancheck/expect_deferred_change_test.go | 169 +++++++++++++++ plancheck/expect_no_deferred_changes.go | 11 +- plancheck/expect_no_deferred_changes_test.go | 198 ++++++++++++++++++ tfversion/skip_if_not_prerelease.go | 28 +++ tfversion/skip_if_not_prerelease_test.go | 94 +++++++++ 16 files changed, 607 insertions(+), 42 deletions(-) create mode 100644 helper/resource/additional_cli_options.go rename plancheck/{expect_deferred_reason.go => expect_deferred_change.go} (52%) create mode 100644 plancheck/expect_deferred_change_test.go create mode 100644 plancheck/expect_no_deferred_changes_test.go create mode 100644 tfversion/skip_if_not_prerelease.go create mode 100644 tfversion/skip_if_not_prerelease_test.go diff --git a/go.mod b/go.mod index d7935aae8..8e7bdcf07 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,9 @@ require ( github.com/hashicorp/hc-install v0.6.4 github.com/hashicorp/hcl/v2 v2.20.1 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.20.1-0.20240426181105-399b96b1d2ec - github.com/hashicorp/terraform-json v0.21.1-0.20240426182256-aa6b5af6a6ac - github.com/hashicorp/terraform-plugin-go v0.22.2 + github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f + github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133 + github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -56,5 +56,5 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.0 // indirect ) diff --git a/go.sum b/go.sum index 1aefcc738..5fc7f9d61 100644 --- a/go.sum +++ b/go.sum @@ -68,12 +68,12 @@ github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdx github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.20.1-0.20240426181105-399b96b1d2ec h1:zCouqvr8Yw0uyAq+UekEcREozRnwZaUVet9uCajcCbk= -github.com/hashicorp/terraform-exec v0.20.1-0.20240426181105-399b96b1d2ec/go.mod h1:rHqaL9Y7oPlRDZffl2xr/UkSrIhKL2l9G5P81Sza7Ts= -github.com/hashicorp/terraform-json v0.21.1-0.20240426182256-aa6b5af6a6ac h1:V6nEe62yvDbP55vM9Oc1IzqB6JscgkBM6JjjtZG5HuA= -github.com/hashicorp/terraform-json v0.21.1-0.20240426182256-aa6b5af6a6ac/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= -github.com/hashicorp/terraform-plugin-go v0.22.2 h1:5o8uveu6eZUf5J7xGPV0eY0TPXg3qpmwX9sce03Bxnc= -github.com/hashicorp/terraform-plugin-go v0.22.2/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM= +github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f h1:wxG+nUAXpW7WMnBjM3keggqx4NVSm3zQHlqgjUrga8Q= +github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f/go.mod h1:rHqaL9Y7oPlRDZffl2xr/UkSrIhKL2l9G5P81Sza7Ts= +github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133 h1:2YwEdKG+W9UVbfj+q6UFFtPd8m1r9bTq/Xct4bkuELM= +github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c h1:TG3MBcQOW3+h7QjtY61HicrSbcyALuJdCF7wgjQIz7w= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 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= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helper/resource/additional_cli_options.go b/helper/resource/additional_cli_options.go new file mode 100644 index 000000000..62578edef --- /dev/null +++ b/helper/resource/additional_cli_options.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +// AdditionalCLIOptions allows an intentionally limited set of options to be passed +// to the Terraform CLI when executing test steps. +type AdditionalCLIOptions struct { + // Apply represents options to be passed to the `terraform apply` command. + Apply ApplyOptions + + // Plan represents options to be passed to the `terraform plan` command. + Plan PlanOptions +} + +// ApplyOptions represents options to be passed to the `terraform apply` command. +type ApplyOptions struct { + // AllowDeferral will pass the experimental `-allow-deferral` flag to the apply command. + AllowDeferral bool +} + +// PlanOptions represents options to be passed to the `terraform plan` command. +type PlanOptions struct { + // AllowDeferral will pass the experimental `-allow-deferral` flag to the plan command. + AllowDeferral bool +} diff --git a/helper/resource/testing.go b/helper/resource/testing.go index b35cee820..9e1961a46 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -17,7 +17,6 @@ import ( "github.com/mitchellh/go-testing-interface" - "github.com/hashicorp/terraform-exec/tfexec" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -442,6 +441,10 @@ type TestCase struct { // set to "1", to persist any working directory files. Otherwise, this directory is // automatically cleaned up at the end of the TestCase. WorkingDir string + + // AdditionalCLIOptions allows an intentionally limited set of options to be passed + // to the Terraform CLI when executing test steps. + AdditionalCLIOptions *AdditionalCLIOptions } // ExternalProvider holds information about third-party providers that should @@ -777,17 +780,6 @@ type TestStep struct { // for performing import testing where the prior TestStep configuration // contained a provider outside the one under test. ExternalProviders map[string]ExternalProvider - - // TODO: doc - AdditionalCLIFlags AdditionalCLIFlags -} - -// TODO: doc -type AdditionalCLIFlags struct { - // TODO: doc - Apply []tfexec.ApplyOption - // TODO: doc - Plan []tfexec.PlanOption } // ConfigPlanChecks defines the different points in a Config TestStep when plan checks can be run. diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index 6dc9cdcc7..1456f7fba 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -108,7 +108,10 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint opts = append(opts, tfexec.Destroy(true)) } - opts = append(opts, step.AdditionalCLIFlags.Plan...) + if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + return wd.CreatePlan(ctx, opts...) }, wd, providers) if err != nil { @@ -170,7 +173,13 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // Apply the diff, creating real resources err = runProviderCommand(ctx, t, func() error { - return wd.Apply(ctx, step.AdditionalCLIFlags.Apply...) + var opts []tfexec.ApplyOption + + if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Apply.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + + return wd.Apply(ctx, opts...) }, wd, providers) if err != nil { if step.Destroy { @@ -241,7 +250,10 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint opts = append(opts, tfexec.Destroy(true)) } - opts = append(opts, step.AdditionalCLIFlags.Plan...) + if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + return wd.CreatePlan(ctx, opts...) }, wd, providers) if err != nil { @@ -306,7 +318,11 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint opts = append(opts, tfexec.Refresh(false)) } } - opts = append(opts, step.AdditionalCLIFlags.Plan...) + + if c.AdditionalCLIOptions != nil && c.AdditionalCLIOptions.Plan.AllowDeferral { + opts = append(opts, tfexec.AllowDeferral(true)) + } + return wd.CreatePlan(ctx, opts...) }, wd, providers) if err != nil { diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 2c762a3a3..0cff33408 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -258,7 +258,6 @@ func (wd *WorkingDir) CreatePlan(ctx context.Context, opts ...tfexec.PlanOption) // this will apply the saved plan. Otherwise, it will implicitly create a new // plan and apply it. func (wd *WorkingDir) Apply(ctx context.Context, opts ...tfexec.ApplyOption) error { - // TODO: dedupe? args := []tfexec.ApplyOption{tfexec.Reattach(wd.reattachInfo), tfexec.Refresh(false)} args = append(args, opts...) if wd.HasSavedPlan() { diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go index 1e0dd017d..4cc3f84ad 100644 --- a/internal/testing/testsdk/providerserver/providerserver.go +++ b/internal/testing/testsdk/providerserver/providerserver.go @@ -58,6 +58,18 @@ type ProviderServer struct { Provider provider.Provider } +func (s ProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { + return &tfprotov6.CallFunctionResponse{}, nil +} + +func (s ProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) { + return &tfprotov6.GetFunctionsResponse{}, nil +} + +func (s ProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { + return &tfprotov6.MoveResourceStateResponse{}, nil +} + func (s ProviderServer) GetMetadata(ctx context.Context, request *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { resp := &tfprotov6.GetMetadataResponse{ ServerCapabilities: &tfprotov6.ServerCapabilities{ @@ -448,6 +460,7 @@ func (s ProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.P resp.Diagnostics = planResp.Diagnostics resp.RequiresReplace = planResp.RequiresReplace + resp.Deferred = planResp.Deferred if len(resp.Diagnostics) > 0 { return resp, nil diff --git a/internal/testing/testsdk/providerserver/providerserver_protov5.go b/internal/testing/testsdk/providerserver/providerserver_protov5.go index 1c945f68f..4e6452b67 100644 --- a/internal/testing/testsdk/providerserver/providerserver_protov5.go +++ b/internal/testing/testsdk/providerserver/providerserver_protov5.go @@ -36,6 +36,20 @@ type Protov5ProviderServer struct { Provider provider.Protov5Provider } +// CallFunction implements tfprotov5.ProviderServer. +func (s Protov5ProviderServer) CallFunction(ctx context.Context, req *tfprotov5.CallFunctionRequest) (*tfprotov5.CallFunctionResponse, error) { + return &tfprotov5.CallFunctionResponse{}, nil +} + +// GetFunctions implements tfprotov5.ProviderServer. +func (s Protov5ProviderServer) GetFunctions(ctx context.Context, req *tfprotov5.GetFunctionsRequest) (*tfprotov5.GetFunctionsResponse, error) { + return &tfprotov5.GetFunctionsResponse{}, nil +} + +func (s Protov5ProviderServer) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) { + return &tfprotov5.MoveResourceStateResponse{}, nil +} + func (s Protov5ProviderServer) GetMetadata(ctx context.Context, request *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { return &tfprotov5.GetMetadataResponse{}, nil } diff --git a/internal/testing/testsdk/resource/resource.go b/internal/testing/testsdk/resource/resource.go index f053b135f..5fea34468 100644 --- a/internal/testing/testsdk/resource/resource.go +++ b/internal/testing/testsdk/resource/resource.go @@ -55,6 +55,7 @@ type PlanChangeRequest struct { } type PlanChangeResponse struct { + Deferred *tfprotov6.Deferred Diagnostics []*tfprotov6.Diagnostic PlannedState tftypes.Value RequiresReplace []*tftypes.AttributePath diff --git a/plancheck/deferred_reason.go b/plancheck/deferred_reason.go index 86e8e362b..4787a8c3e 100644 --- a/plancheck/deferred_reason.go +++ b/plancheck/deferred_reason.go @@ -3,14 +3,19 @@ package plancheck -// TODO: doc +// DeferredReason is a string stored in the plan file which indicates why Terraform +// is deferring a change for a resource. type DeferredReason string const ( - // TODO: doc + // DeferredReasonResourceConfigUnknown is used to indicate that the resource configuration + // is partially unknown and the real values need to be known before the change can be planned. DeferredReasonResourceConfigUnknown DeferredReason = "resource_config_unknown" - // TODO: doc + + // DeferredReasonProviderConfigUnknown is used to indicate that the provider configuration + // is partially unknown and the real values need to be known before the change can be planned. DeferredReasonProviderConfigUnknown DeferredReason = "provider_config_unknown" - // TODO: doc + + // DeferredReasonAbsentPrereq is used to indicate that a hard dependency has not been satisfied. DeferredReasonAbsentPrereq DeferredReason = "absent_prereq" ) diff --git a/plancheck/expect_deferred_reason.go b/plancheck/expect_deferred_change.go similarity index 52% rename from plancheck/expect_deferred_reason.go rename to plancheck/expect_deferred_change.go index b222e27b9..14310ca31 100644 --- a/plancheck/expect_deferred_reason.go +++ b/plancheck/expect_deferred_change.go @@ -8,15 +8,15 @@ import ( "fmt" ) -var _ PlanCheck = expectDeferredReason{} +var _ PlanCheck = expectDeferredChange{} -type expectDeferredReason struct { +type expectDeferredChange struct { resourceAddress string reason DeferredReason } // CheckPlan implements the plan check logic. -func (e expectDeferredReason) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { +func (e expectDeferredChange) CheckPlan(ctx context.Context, req CheckPlanRequest, resp *CheckPlanResponse) { foundResource := false for _, dc := range req.Plan.DeferredChanges { @@ -25,7 +25,7 @@ func (e expectDeferredReason) CheckPlan(ctx context.Context, req CheckPlanReques } if e.reason != DeferredReason(dc.Reason) { - resp.Error = fmt.Errorf("'%s' - expected %s, got deferred reason: %s", dc.ResourceChange.Address, e.reason, dc.Reason) + resp.Error = fmt.Errorf("'%s' - expected %q, got deferred reason: %q", dc.ResourceChange.Address, e.reason, dc.Reason) return } @@ -34,14 +34,15 @@ func (e expectDeferredReason) CheckPlan(ctx context.Context, req CheckPlanReques } if !foundResource { - resp.Error = fmt.Errorf("%s - Resource not found in plan DeferredChanges", e.resourceAddress) + resp.Error = fmt.Errorf("%s - No deferred changes found for resource", e.resourceAddress) return } } -// TODO: doc -func ExpectDeferredReason(resourceAddress string, reason DeferredReason) PlanCheck { - return expectDeferredReason{ +// ExpectDeferredChange returns a plan check that asserts that a given resource will have a +// deferred change in the plan with the given reason. +func ExpectDeferredChange(resourceAddress string, reason DeferredReason) PlanCheck { + return expectDeferredChange{ resourceAddress: resourceAddress, reason: reason, } diff --git a/plancheck/expect_deferred_change_test.go b/plancheck/expect_deferred_change_test.go new file mode 100644 index 000000000..05baa0671 --- /dev/null +++ b/plancheck/expect_deferred_change_test.go @@ -0,0 +1,169 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectDeferredChange_Reason_Match(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotPrerelease(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectDeferredChange("test_resource.test", plancheck.DeferredReasonResourceConfigUnknown), + }, + }, + }, + }, + }) +} + +func Test_ExpectDeferredChange_Reason_NoMatch(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotPrerelease(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectDeferredChange("test_resource.test", plancheck.DeferredReasonProviderConfigUnknown), + }, + }, + ExpectError: regexp.MustCompile(`expected "provider_config_unknown", got deferred reason: "absent_prereq"`), + }, + }, + }) +} + +func Test_ExpectDeferredChange_NoDeferredChanges(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotPrerelease(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectDeferredChange("test_resource.test", plancheck.DeferredReasonProviderConfigUnknown), + }, + }, + ExpectError: regexp.MustCompile(`No deferred changes found for resource`), + }, + }, + }) +} diff --git a/plancheck/expect_no_deferred_changes.go b/plancheck/expect_no_deferred_changes.go index fceb4946e..c4b4973e0 100644 --- a/plancheck/expect_no_deferred_changes.go +++ b/plancheck/expect_no_deferred_changes.go @@ -30,9 +30,18 @@ func (e expectNoDeferredChanges) CheckPlan(ctx context.Context, req CheckPlanReq } resp.Error = errors.Join(result...) + if resp.Error != nil { + return + } + + if !req.Plan.Complete { + resp.Error = errors.New("expected plan to be marked as complete, but complete was \"false\", indicating that at least one more plan/apply round is needed to converge.") + return + } } -// TODO: doc +// ExpectNoDeferredChanges returns a plan check that asserts that there are no deffered changes +// for any resources in the plan. func ExpectNoDeferredChanges() PlanCheck { return expectNoDeferredChanges{} } diff --git a/plancheck/expect_no_deferred_changes_test.go b/plancheck/expect_no_deferred_changes_test.go new file mode 100644 index 000000000..b3b5bfc23 --- /dev/null +++ b/plancheck/expect_no_deferred_changes_test.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package plancheck_test + +import ( + "context" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_ExpectNoDeferredChange(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotPrerelease(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" { + id = "hello" + }`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNoDeferredChanges(), + }, + }, + }, + }, + }) +} + +func Test_ExpectNoDeferredChange_OneDeferral(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotPrerelease(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: `resource "test_resource" "test" {}`, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNoDeferredChanges(), + }, + }, + ExpectError: regexp.MustCompile(`expected no deferred changes, but resource "test_resource.test" is deferred with reason: "resource_config_unknown"`), + }, + }, + }) +} + +func Test_ExpectNoDeferredChange_MultipleDeferrals(t *testing.T) { + t.Parallel() + + r.Test(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_9_0), + tfversion.SkipIfNotPrerelease(), + }, + AdditionalCLIOptions: &r.AdditionalCLIOptions{ + Plan: r.PlanOptions{AllowDeferral: true}, + Apply: r.ApplyOptions{AllowDeferral: true}, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{ + Resources: map[string]testprovider.Resource{ + "test_resource_one": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonResourceConfigUnknown, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + "test_resource_two": { + PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) { + resp.Deferred = &tfprotov6.Deferred{ + Reason: tfprotov6.DeferredReasonAbsentPrereq, + } + }, + SchemaResponse: &resource.SchemaResponse{ + Schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Type: tftypes.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }), + }, + Steps: []r.TestStep{ + { + Config: ` + resource "test_resource_one" "test" {} + + resource "test_resource_two" "test" {} + `, + ConfigPlanChecks: r.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectNoDeferredChanges(), + }, + }, + ExpectError: regexp.MustCompile( + `expected no deferred changes, but resource "test_resource_one.test" is deferred with reason: "resource_config_unknown"\n` + + `expected no deferred changes, but resource "test_resource_two.test" is deferred with reason: "absent_prereq"`, + ), + }, + }, + }) +} diff --git a/tfversion/skip_if_not_prerelease.go b/tfversion/skip_if_not_prerelease.go new file mode 100644 index 000000000..c49400b29 --- /dev/null +++ b/tfversion/skip_if_not_prerelease.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" +) + +// SkipIfNotPrerelease will skip (pass) the test if the Terraform CLI +// version does not include prerelease information. This will include builds +// of Terraform that are from source. (e.g. 1.8.0-dev) +func SkipIfNotPrerelease() TerraformVersionCheck { + return skipIfNotPrereleaseCheck{} +} + +// skipIfNotPrereleaseCheck implements the TerraformVersionCheck interface +type skipIfNotPrereleaseCheck struct{} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s skipIfNotPrereleaseCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + if req.TerraformVersion.Prerelease() != "" { + return + } + + resp.Skip = fmt.Sprintf("Terraform CLI version %s is not a prerelease build: skipping test.", req.TerraformVersion) +} diff --git a/tfversion/skip_if_not_prerelease_test.go b/tfversion/skip_if_not_prerelease_test.go new file mode 100644 index 000000000..545017ed0 --- /dev/null +++ b/tfversion/skip_if_not_prerelease_test.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_SkipIfNotPrerelease_SkipTest_Stable(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIfNotPrerelease_RunTest_Alpha(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.9.0-alpha20240501") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIfNotPrerelease_RunTest_Beta1(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-beta1") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} +func Test_SkipIfNotPrerelease_RunTest_RC(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_PATH", "") + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.8.0-rc2") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": providerserver.NewProviderServer(testprovider.Provider{}), + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIfNotPrerelease(), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} From 809a5b07cbeaaf0f4df12cfdcbecd9920756ad0b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 3 May 2024 16:24:40 -0400 Subject: [PATCH 3/7] add changelogs --- .changes/unreleased/ENHANCEMENTS-20240503-161709.yaml | 6 ++++++ .changes/unreleased/FEATURES-20240503-161531.yaml | 6 ++++++ .changes/unreleased/FEATURES-20240503-161802.yaml | 6 ++++++ 3 files changed, 18 insertions(+) create mode 100644 .changes/unreleased/ENHANCEMENTS-20240503-161709.yaml create mode 100644 .changes/unreleased/FEATURES-20240503-161531.yaml create mode 100644 .changes/unreleased/FEATURES-20240503-161802.yaml diff --git a/.changes/unreleased/ENHANCEMENTS-20240503-161709.yaml b/.changes/unreleased/ENHANCEMENTS-20240503-161709.yaml new file mode 100644 index 000000000..dff8d4adf --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20240503-161709.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'helper/resource: Added `(TestCase).AdditionalCLIOptions` with `AllowDeferral` + option for plan and apply commands.' +time: 2024-05-03T16:17:09.64792-04:00 +custom: + Issue: "331" diff --git a/.changes/unreleased/FEATURES-20240503-161531.yaml b/.changes/unreleased/FEATURES-20240503-161531.yaml new file mode 100644 index 000000000..edd8c036f --- /dev/null +++ b/.changes/unreleased/FEATURES-20240503-161531.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'plancheck: Added `ExpectDeferredChange` and `ExpectNoDeferredChanges` checks + for experimental deferred action support.' +time: 2024-05-03T16:15:31.03438-04:00 +custom: + Issue: "331" diff --git a/.changes/unreleased/FEATURES-20240503-161802.yaml b/.changes/unreleased/FEATURES-20240503-161802.yaml new file mode 100644 index 000000000..a06f5f78c --- /dev/null +++ b/.changes/unreleased/FEATURES-20240503-161802.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `SkipIfNotPrerelease` version check for testing experimental + features of prerelease Terraform builds.' +time: 2024-05-03T16:18:02.132794-04:00 +custom: + Issue: "331" From 25748ca04c8ad146e7d7e545f216c0715486433e Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 16:38:17 -0400 Subject: [PATCH 4/7] update terraform-plugin-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8e7bdcf07..2112ecfe2 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133 - github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c + github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index 5fc7f9d61..ddb85bea3 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f h1:wxG github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f/go.mod h1:rHqaL9Y7oPlRDZffl2xr/UkSrIhKL2l9G5P81Sza7Ts= github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133 h1:2YwEdKG+W9UVbfj+q6UFFtPd8m1r9bTq/Xct4bkuELM= github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= -github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c h1:TG3MBcQOW3+h7QjtY61HicrSbcyALuJdCF7wgjQIz7w= -github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= +github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 h1:qHprzXy/As0rxedphECBEQAh3R4yp6pKksKHcqZx5G8= From 0d6d1dc571190373995e7d80ae68cc40413b669c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 17:33:52 -0400 Subject: [PATCH 5/7] spelling fix --- plancheck/expect_no_deferred_changes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plancheck/expect_no_deferred_changes.go b/plancheck/expect_no_deferred_changes.go index c4b4973e0..d924b11c4 100644 --- a/plancheck/expect_no_deferred_changes.go +++ b/plancheck/expect_no_deferred_changes.go @@ -40,7 +40,7 @@ func (e expectNoDeferredChanges) CheckPlan(ctx context.Context, req CheckPlanReq } } -// ExpectNoDeferredChanges returns a plan check that asserts that there are no deffered changes +// ExpectNoDeferredChanges returns a plan check that asserts that there are no deferred changes // for any resources in the plan. func ExpectNoDeferredChanges() PlanCheck { return expectNoDeferredChanges{} From 6a4cb926e0e5ae29b416fcb45116f52cd84de42f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 14 May 2024 14:22:42 -0400 Subject: [PATCH 6/7] update `terraform-json` --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 70f3e6a81..07afa63f3 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/hcl/v2 v2.20.1 github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f - github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133 + github.com/hashicorp/terraform-json v0.22.0 github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 diff --git a/go.sum b/go.sum index b9bd220ce..18e125dab 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f h1:wxG+nUAXpW7WMnBjM3keggqx4NVSm3zQHlqgjUrga8Q= github.com/hashicorp/terraform-exec v0.20.1-0.20240501214311-a1abb29f777f/go.mod h1:rHqaL9Y7oPlRDZffl2xr/UkSrIhKL2l9G5P81Sza7Ts= -github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133 h1:2YwEdKG+W9UVbfj+q6UFFtPd8m1r9bTq/Xct4bkuELM= -github.com/hashicorp/terraform-json v0.21.1-0.20240502194054-3b8a921d9133/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= +github.com/hashicorp/terraform-json v0.22.0 h1:cTZejr05cbovFmQ05MeJ43CPrn/+1hUBG7KDTcRwBc0= +github.com/hashicorp/terraform-json v0.22.0/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= From 98479a946af54f7c6196ef7b59dbe51dafe279f7 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 17 May 2024 11:33:17 -0400 Subject: [PATCH 7/7] switch to pointer bool value --- plancheck/expect_no_deferred_changes.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plancheck/expect_no_deferred_changes.go b/plancheck/expect_no_deferred_changes.go index d924b11c4..726ea6802 100644 --- a/plancheck/expect_no_deferred_changes.go +++ b/plancheck/expect_no_deferred_changes.go @@ -34,7 +34,12 @@ func (e expectNoDeferredChanges) CheckPlan(ctx context.Context, req CheckPlanReq return } - if !req.Plan.Complete { + if req.Plan.Complete == nil { + resp.Error = errors.New("expected plan to be marked as complete, but complete field was not set in plan (nil). This indicates that the plan was created with a version of Terraform older than 1.8, which does not support the complete field.") + return + } + + if !*req.Plan.Complete { resp.Error = errors.New("expected plan to be marked as complete, but complete was \"false\", indicating that at least one more plan/apply round is needed to converge.") return }