From 4ee9165e34aba4234e36a28a938cc5aa6ce5c6a0 Mon Sep 17 00:00:00 2001 From: Selena Goods Date: Tue, 26 Nov 2024 15:56:27 -0500 Subject: [PATCH 01/35] Add SDKv2 resource with write-only attribute and tests --- go.mod | 8 +- go.sum | 22 ++- internal/protocolprovider/server.go | 5 +- internal/sdkv2provider/provider.go | 9 +- .../sdkv2provider/resource_user_write_only.go | 137 ++++++++++++++++++ .../resource_user_write_only_test.go | 100 +++++++++++++ internal/sdkv2testingprovider/provider.go | 7 + main.go | 14 +- 8 files changed, 284 insertions(+), 18 deletions(-) create mode 100644 internal/sdkv2provider/resource_user_write_only.go create mode 100644 internal/sdkv2provider/resource_user_write_only_test.go diff --git a/go.mod b/go.mod index eed7115..652111e 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/hashicorp/terraform-plugin-framework v1.13.0 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 - github.com/hashicorp/terraform-plugin-go v0.25.0 - github.com/hashicorp/terraform-plugin-mux v0.17.0 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 + github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0 + github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241125203040-95ad936456f6 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9 github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/zclconf/go-cty v1.15.0 ) @@ -64,5 +64,5 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect ) diff --git a/go.sum b/go.sum index cd5fa21..3b9ed45 100644 --- a/go.sum +++ b/go.sum @@ -87,14 +87,20 @@ github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaK github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 h1:v3DapR8gsp3EM8fKMh6up9cJUFQ2iRaFsYLP8UJnCco= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0/go.mod h1:c3PnGE9pHBDfdEVG9t1S1C9ia5LW+gkFR0CygXlM8ak= -github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= -github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0 h1:92SnqDgZi6TgHrJlbP5UicGWgoZO+QQUrZyzwW2Ztqs= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0/go.mod h1:f8P2pHGkZrtdKLpCI2qIvrewUY+c4nTvtayqjJR9IcY= 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-mux v0.17.0 h1:/J3vv3Ps2ISkbLPiZOLspFcIZ0v5ycUXCEQScudGCCw= -github.com/hashicorp/terraform-plugin-mux v0.17.0/go.mod h1:yWuM9U1Jg8DryNfvCp+lH70WcYv6D8aooQxxxIzFDsE= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 h1:wyKCCtn6pBBL46c1uIIBNUOWlNfYXfXpVo16iDyLp8Y= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0/go.mod h1:B0Al8NyYVr8Mp/KLwssKXG1RqnTk7FySqSn4fRuLNgw= +github.com/hashicorp/terraform-plugin-mux v0.16.0 h1:RCzXHGDYwUwwqfYYWJKBFaS3fQsWn/ZECEiW7p2023I= +github.com/hashicorp/terraform-plugin-mux v0.16.0/go.mod h1:PF79mAsPc8CpusXPfEVa4X8PtkB+ngWoiUClMrNZlYo= +github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241125203040-95ad936456f6 h1:LUGGYEO+C3rfg4wtjADOu4a/viMliBMUVXayw27ff54= +github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241125203040-95ad936456f6/go.mod h1:Z0o8Mb8Z+XGN2BPVzbuqFOoqdBf961s1qaiH8W8Jd1s= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125194050-c839940413ef h1:bBcha3zET8VxpLmadFol4fj+pY5aTfTh8HnMvv0UTZY= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125194050-c839940413ef/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125231335-15e2d7db0855 h1:c119BCmrwU9Gf1KTowsLbuF15qy+2UB5JozS/Ku1e7Y= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125231335-15e2d7db0855/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9 h1:6EmxSSs3nMfNexYE6yS5M2ObXO+bULz8Pcq4W0O0KzA= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -218,8 +224,8 @@ google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/internal/protocolprovider/server.go b/internal/protocolprovider/server.go index f50bf54..6fa8bca 100644 --- a/internal/protocolprovider/server.go +++ b/internal/protocolprovider/server.go @@ -95,7 +95,10 @@ func (s *server) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRe func Server() tfprotov5.ProviderServer { return &server{ providerSchema: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{}, + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + {Name: "deferral", Type: tftypes.Bool, Optional: true}}, + }, }, dataSourceSchemas: map[string]*tfprotov5.Schema{ "corner_time": { diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index 576c3ca..ec62c30 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -26,10 +26,11 @@ func New() *schema.Provider { "corner_regions_cty": dataSourceRegionsCty(), }, ResourcesMap: map[string]*schema.Resource{ - "corner_user": resourceUser(), - "corner_bigint": resourceBigint(), - "corner_user_cty": resourceUserCty(), - "corner_deferred_action": resourceDeferredAction(), + "corner_user": resourceUser(), + "corner_user_writeonly": resourceUserWriteOnly(), + "corner_bigint": resourceBigint(), + "corner_user_cty": resourceUserCty(), + "corner_deferred_action": resourceDeferredAction(), "corner_deferred_action_plan_modification": resourceDeferredActionPlanModification(), }, } diff --git a/internal/sdkv2provider/resource_user_write_only.go b/internal/sdkv2provider/resource_user_write_only.go new file mode 100644 index 0000000..8f7cf47 --- /dev/null +++ b/internal/sdkv2provider/resource_user_write_only.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//nolint:forcetypeassert // Test SDK provider +package sdkv2 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-corner/internal/backend" +) + +func resourceUserWriteOnly() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceUserWriteOnlyCreate, + ReadContext: resourceUserWriteOnlyRead, + UpdateContext: resourceUserWriteOnlyUpdate, + DeleteContext: resourceUserWriteOnlyDelete, + + Schema: map[string]*schema.Schema{ + "email": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "age": { + Type: schema.TypeInt, + Required: true, + }, + "password": { + Type: schema.TypeString, + Optional: true, + WriteOnly: true, + }, + "saved_password": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceUserWriteOnlyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*backend.Client) + newUser := &backend.User{ + Email: d.Get("email").(string), + Name: d.Get("name").(string), + Age: d.Get("age").(int), + } + + err := client.CreateUser(newUser) + if err != nil { + return diag.FromErr(err) + } + + password := d.Get("password").(string) + + err = d.Set("password", password) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("saved_password", password) + if err != nil { + return diag.FromErr(err) + } + + return resourceUserWriteOnlyRead(ctx, d, meta) +} + +func resourceUserWriteOnlyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*backend.Client) + + email := d.Get("email").(string) + + p, err := client.ReadUser(email) + if err != nil { + return diag.FromErr(err) + } + + if p == nil { + return nil + } + + d.SetId(email) + + err = d.Set("name", p.Name) + if err != nil { + return diag.FromErr(err) + } + err = d.Set("age", p.Age) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceUserWriteOnlyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*backend.Client) + + user := &backend.User{ + Email: d.Get("email").(string), + Name: d.Get("name").(string), + Age: d.Get("age").(int), + } + + err := client.UpdateUser(user) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceUserWriteOnlyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*backend.Client) + + user := &backend.User{ + Email: d.Get("email").(string), + Name: d.Get("name").(string), + Age: d.Get("age").(int), + } + + err := client.DeleteUser(user) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/internal/sdkv2provider/resource_user_write_only_test.go b/internal/sdkv2provider/resource_user_write_only_test.go new file mode 100644 index 0000000..534a0a9 --- /dev/null +++ b/internal/sdkv2provider/resource_user_write_only_test.go @@ -0,0 +1,100 @@ +package sdkv2 + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + framework "github.com/hashicorp/terraform-provider-corner/internal/framework5provider" +) + +func TestSchemaWriteOnly_basic(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(framework.New()), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `ephemeral "framework_schema" "test" { + string_attribute = "test" + } + + resource "corner_user_writeonly" "foo" { + email = "ford@prefect.co" + name = "Ford Prefect" + age = 200 + password = ephemeral.framework_schema.test.string_attribute + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), + plancheck.ExpectUnknownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password"), knownvalue.NotNull()), + }, + }, + }, + }) +} + +func TestSchemaWriteOnly_randomPassword(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(framework.New()), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `ephemeral "random_password" "test" { + length = 20 + } + + resource "corner_user_writeonly" "foo" { + email = "ford@prefect.co" + name = "Ford Prefect" + age = 200 + password = ephemeral.random_password.test.result + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), + plancheck.ExpectUnknownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password"), knownvalue.NotNull()), + }, + }, + }, + }) +} diff --git a/internal/sdkv2testingprovider/provider.go b/internal/sdkv2testingprovider/provider.go index f2b28b6..0cb7a6f 100644 --- a/internal/sdkv2testingprovider/provider.go +++ b/internal/sdkv2testingprovider/provider.go @@ -8,11 +8,18 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-corner/internal/backend" ) func New() *schema.Provider { p := &schema.Provider{ + Schema: map[string]*schema.Schema{ + "deferral": { + Type: schema.TypeBool, + Optional: true, + }, + }, DataSourcesMap: map[string]*schema.Resource{ "corner_regions": dataSourceRegions(), "corner_bigint": dataSourceBigint(), diff --git a/main.go b/main.go index dd522f1..7aabcfb 100644 --- a/main.go +++ b/main.go @@ -5,17 +5,25 @@ package main import ( "context" + "flag" "log" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" + protocol "github.com/hashicorp/terraform-provider-corner/internal/protocolprovider" sdkv2 "github.com/hashicorp/terraform-provider-corner/internal/sdkv2provider" ) func main() { ctx := context.Background() + + var debug bool + + flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") + flag.Parse() + providers := []func() tfprotov5.ProviderServer{ protocol.Server, sdkv2.New().GRPCProvider, @@ -27,7 +35,11 @@ func main() { log.Fatalf("unable to create provider: %s", err) } - err = tf5server.Serve("registry.terraform.io/hashicorp/corner", muxServer.ProviderServer) + if debug { + err = tf5server.Serve("registry.terraform.io/hashicorp/corner", muxServer.ProviderServer, tf5server.WithManagedDebug()) + } else { + err = tf5server.Serve("registry.terraform.io/hashicorp/corner", muxServer.ProviderServer) + } if err != nil { log.Fatalf("unable to serve provider: %s", err) From 699359a34573343de36b55d50134265b3cbf8f56 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 17 Dec 2024 13:42:40 -0500 Subject: [PATCH 02/35] updated go mod --- go.mod | 14 +++++++------- go.sum | 30 ++++++++++++------------------ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index d9cfe53..1999ebb 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,13 @@ go 1.22.7 require ( github.com/hashicorp/go-memdb v1.3.4 + github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-json v0.23.0 - github.com/hashicorp/terraform-plugin-framework v1.13.0 + github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 - github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0 - github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241125203040-95ad936456f6 + github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa + github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9 github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/zclconf/go-cty v1.15.1 @@ -33,7 +34,6 @@ require ( github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hc-install v0.9.0 // indirect github.com/hashicorp/hcl/v2 v2.23.0 // indirect @@ -56,13 +56,13 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/crypto v0.29.0 // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.20.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/grpc v1.67.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.35.2 // indirect ) diff --git a/go.sum b/go.sum index 1087820..0f7b289 100644 --- a/go.sum +++ b/go.sum @@ -81,24 +81,18 @@ github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVW github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI= github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= -github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= -github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= +github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08 h1:JS11x8PqVqNbFMa0g6BNYcBxJcWy9lc2EI0ksknL+Tw= +github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08/go.mod h1:qBKUqe1lv1NZcsO5pBjiKd5YuNDUeqvV1w8w5df/8WI= github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E= github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 h1:v3DapR8gsp3EM8fKMh6up9cJUFQ2iRaFsYLP8UJnCco= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0/go.mod h1:c3PnGE9pHBDfdEVG9t1S1C9ia5LW+gkFR0CygXlM8ak= -github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0 h1:92SnqDgZi6TgHrJlbP5UicGWgoZO+QQUrZyzwW2Ztqs= -github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241125193751-9019c19499e0/go.mod h1:f8P2pHGkZrtdKLpCI2qIvrewUY+c4nTvtayqjJR9IcY= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa h1:GOXZVYZrfDrWxZMHdSNqZKDwayH8WBBtyOLx25ekwv8= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa/go.mod h1:OKJU8uauqiLVRWjlFB0KIgK++baq26qfvOU1IVycx9k= 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-mux v0.16.0 h1:RCzXHGDYwUwwqfYYWJKBFaS3fQsWn/ZECEiW7p2023I= -github.com/hashicorp/terraform-plugin-mux v0.16.0/go.mod h1:PF79mAsPc8CpusXPfEVa4X8PtkB+ngWoiUClMrNZlYo= -github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241125203040-95ad936456f6 h1:LUGGYEO+C3rfg4wtjADOu4a/viMliBMUVXayw27ff54= -github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241125203040-95ad936456f6/go.mod h1:Z0o8Mb8Z+XGN2BPVzbuqFOoqdBf961s1qaiH8W8Jd1s= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125194050-c839940413ef h1:bBcha3zET8VxpLmadFol4fj+pY5aTfTh8HnMvv0UTZY= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125194050-c839940413ef/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125231335-15e2d7db0855 h1:c119BCmrwU9Gf1KTowsLbuF15qy+2UB5JozS/Ku1e7Y= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241125231335-15e2d7db0855/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= +github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de h1:qjSTbKECIWAEO5hBGqabykC/xwrAYIrD7fKSVKldFhY= +github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de/go.mod h1:To/3Wdozxp5QLvgcR4lKOdfiBcG70eoFZepcXro+nPQ= github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9 h1:6EmxSSs3nMfNexYE6yS5M2ObXO+bULz8Pcq4W0O0KzA= github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= @@ -178,8 +172,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -218,10 +212,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= 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.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= From ef84121258bc0f489f5d103b94008332b2d52bd6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 17 Dec 2024 17:35:40 -0500 Subject: [PATCH 03/35] add some initial smoke tests for write-only framework --- go.mod | 1 + go.sum | 2 + internal/framework5provider/provider.go | 1 + .../framework5provider/writeonly_resource.go | 210 +++++++++++++ .../writeonly_resource_test.go | 204 +++++++++++++ internal/framework6provider/provider.go | 1 + .../framework6provider/writeonly_resource.go | 285 ++++++++++++++++++ .../writeonly_resource_test.go | 259 ++++++++++++++++ 8 files changed, 963 insertions(+) create mode 100644 internal/framework5provider/writeonly_resource.go create mode 100644 internal/framework5provider/writeonly_resource_test.go create mode 100644 internal/framework6provider/writeonly_resource.go create mode 100644 internal/framework6provider/writeonly_resource_test.go diff --git a/go.mod b/go.mod index 1999ebb..18e249b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-json v0.23.0 github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08 + github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa diff --git a/go.sum b/go.sum index 0f7b289..31ecfcd 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,8 @@ github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2 github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08 h1:JS11x8PqVqNbFMa0g6BNYcBxJcWy9lc2EI0ksknL+Tw= github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08/go.mod h1:qBKUqe1lv1NZcsO5pBjiKd5YuNDUeqvV1w8w5df/8WI= +github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 h1:Zap24rkky7SvNGGNYHMKFhAriP6+6riI21BMYOYgLRE= +github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0/go.mod h1:CYPq+I5bWsmI8021VJY85hAyOeiEEQpdGW+NapdQn7A= github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E= github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 h1:v3DapR8gsp3EM8fKMh6up9cJUFQ2iRaFsYLP8UJnCco= diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index 76aa407..507af23 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -91,6 +91,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewTFSDKReflectionResource, NewMoveStateResource, NewSetNestedBlockWithDefaultsResource, + NewWriteOnlyResource, } } diff --git a/internal/framework5provider/writeonly_resource.go b/internal/framework5provider/writeonly_resource.go new file mode 100644 index 0000000..85ac5da --- /dev/null +++ b/internal/framework5provider/writeonly_resource.go @@ -0,0 +1,210 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-nettypes/iptypes" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyResource{} + +func NewWriteOnlyResource() resource.Resource { + return &WriteOnlyResource{} +} + +type WriteOnlyResource struct{} + +// WriteOnlyResource is a smoke test for schema attributes that contain write-only attributes +func (r WriteOnlyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly" +} + +func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "writeonly_custom_ipv6": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + CustomType: iptypes.IPv6AddressType{}, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "writeonly_set": schema.SetAttribute{ + Optional: true, + WriteOnly: true, + ElementType: types.StringType, + }, + }, + Blocks: map[string]schema.Block{ + "nested_block_list": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "double_nested_object": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "bool_attr": schema.BoolAttribute{ + Required: true, + }, + "writeonly_bool": schema.BoolAttribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan WriteOnlyResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // If the write-only data exists in config, verify the data is what we hardcoded in the resource + resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data WriteOnlyResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnlyResourceModel struct { + WriteOnlyCustomIPv6 iptypes.IPv6Address `tfsdk:"writeonly_custom_ipv6"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` + WriteOnlySet types.Set `tfsdk:"writeonly_set"` + NestedBlockList types.List `tfsdk:"nested_block_list"` +} + +// VerifyWriteOnlyData compares the hardcoded test data for the write-only attributes in this resource, raising +// error diagnostics if the data differs from expectations. Null write-only values will be ignored as all write-only +// attributes are optional +func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { + // Primitive write-only attributes + expectedCustomIPv6 := "::" + expectedString := "fakepassword" + if !m.WriteOnlyCustomIPv6.IsNull() && m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { + return diag.NewAttributeErrorDiagnostic( + path.Root("writeonly_custom_ipv6"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6.ValueString()), + ) + } + if !m.WriteOnlyString.IsNull() && m.WriteOnlyString.ValueString() != expectedString { + return diag.NewAttributeErrorDiagnostic( + path.Root("writeonly_string"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString.ValueString()), + ) + } + + // Collection write-only attribute + expectedSet := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}) + if !m.WriteOnlySet.IsNull() && !m.WriteOnlySet.Equal(expectedSet) { + return diag.NewAttributeErrorDiagnostic( + path.Root("writeonly_set"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedSet, m.WriteOnlySet), + ) + } + + // Nested block with write-only attributes + expectedBlockObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool_attr": types.BoolType, + "writeonly_bool": types.BoolType, + }, + } + expectedBlockListObjType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string_attr": types.StringType, + "writeonly_string": types.StringType, + "double_nested_object": expectedBlockObjectType, + }, + } + expectedListBlock := types.ListValueMust(expectedBlockListObjType, []attr.Value{ + types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ + "string_attr": types.StringValue("hello"), + "writeonly_string": types.StringValue("fakepassword1"), + "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ + "bool_attr": types.BoolValue(true), + "writeonly_bool": types.BoolValue(false), + }), + }), + types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ + "string_attr": types.StringValue("world"), + "writeonly_string": types.StringValue("fakepassword2"), + "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ + "bool_attr": types.BoolValue(false), + "writeonly_bool": types.BoolValue(true), + }), + }), + }) + + if len(m.NestedBlockList.Elements()) > 0 && !m.NestedBlockList.Equal(expectedListBlock) { + return diag.NewAttributeErrorDiagnostic( + path.Root("nested_block_list"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedListBlock, m.NestedBlockList), + ) + } + + return nil +} diff --git a/internal/framework5provider/writeonly_resource_test.go b/internal/framework5provider/writeonly_resource_test.go new file mode 100644 index 0000000..0f2c6ef --- /dev/null +++ b/internal/framework5provider/writeonly_resource_test.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself, to verify the config data is passed to the resource Create function. +// The state check assertions cannot be used for testing this data as write-only data should never be persisted in state. + +func TestWriteOnlyResource_CustomType(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_custom_ipv6 = "::" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestWriteOnlyResource_Primitive(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestWriteOnlyResource_Collection(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_set = ["fake", "password"] + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestWriteOnlyResource_NestedBlock(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + nested_block_list { + string_attr = "hello" + writeonly_string = "fakepassword1" + + double_nested_object { + bool_attr = true + writeonly_bool = false + } + } + nested_block_list { + string_attr = "world" + writeonly_string = "fakepassword2" + + double_nested_object { + bool_attr = false + writeonly_bool = true + } + } + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index e5a3df3..a92aa66 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -91,6 +91,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewMoveStateResource, NewSetNestedBlockWithDefaultsResource, NewSetNestedAttributeWithDefaultsResource, + NewWriteOnlyResource, } } diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go new file mode 100644 index 0000000..705381e --- /dev/null +++ b/internal/framework6provider/writeonly_resource.go @@ -0,0 +1,285 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-nettypes/iptypes" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyResource{} + +func NewWriteOnlyResource() resource.Resource { + return &WriteOnlyResource{} +} + +type WriteOnlyResource struct{} + +// WriteOnlyResource is a smoke test for schema attributes that contain write-only attributes +func (r WriteOnlyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly" +} + +func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "writeonly_custom_ipv6": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + CustomType: iptypes.IPv6AddressType{}, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + "writeonly_set": schema.SetAttribute{ + Optional: true, + WriteOnly: true, + ElementType: types.StringType, + }, + // TODO: At the moment, this raises an invalid plan error in Terraform core (Provider is successfully planning null, core + // is rejecting this null value, instead saying it should be the config value, which is a bug) + // - https://hashicorp.slack.com/archives/C071HC4JJCC/p1734465766242319?thread_ts=1734465749.748579&cid=C071HC4JJCC + // + // "nested_object": schema.SingleNestedAttribute{ + // Optional: true, + // Attributes: map[string]schema.Attribute{ + // "string_attr": schema.StringAttribute{ + // Required: true, + // }, + // "writeonly_float64": schema.Float64Attribute{ + // Required: true, + // WriteOnly: true, + // }, + // }, + // }, + "writeonly_nested_object": schema.SingleNestedAttribute{ + Optional: true, + WriteOnly: true, + Attributes: map[string]schema.Attribute{ + "writeonly_int64": schema.Int64Attribute{ + Required: true, + WriteOnly: true, + }, + "writeonly_bool": schema.BoolAttribute{ + Required: true, + WriteOnly: true, + }, + "writeonly_nested_list": schema.ListNestedAttribute{ + Required: true, + WriteOnly: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "writeonly_string": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "nested_block_list": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Required: true, + WriteOnly: true, + }, + }, + Blocks: map[string]schema.Block{ + "double_nested_object": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "bool_attr": schema.BoolAttribute{ + Required: true, + }, + "writeonly_bool": schema.BoolAttribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan WriteOnlyResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // If the write-only data exists in config, verify the data is what we hardcoded in the resource + resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data WriteOnlyResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnlyResourceModel struct { + WriteOnlyCustomIPv6 iptypes.IPv6Address `tfsdk:"writeonly_custom_ipv6"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` + WriteOnlySet types.Set `tfsdk:"writeonly_set"` + WriteOnlyNestedObject types.Object `tfsdk:"writeonly_nested_object"` + NestedBlockList types.List `tfsdk:"nested_block_list"` +} + +// VerifyWriteOnlyData compares the hardcoded test data for the write-only attributes in this resource, raising +// error diagnostics if the data differs from expectations. Null write-only values will be ignored as all write-only +// attributes are optional +func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { + // Primitive write-only attributes + expectedCustomIPv6 := "::" + expectedString := "fakepassword" + if !m.WriteOnlyCustomIPv6.IsNull() && m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { + return diag.NewAttributeErrorDiagnostic( + path.Root("writeonly_custom_ipv6"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6.ValueString()), + ) + } + if !m.WriteOnlyString.IsNull() && m.WriteOnlyString.ValueString() != expectedString { + return diag.NewAttributeErrorDiagnostic( + path.Root("writeonly_string"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString.ValueString()), + ) + } + + // Collection write-only attribute + expectedSet := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}) + if !m.WriteOnlySet.IsNull() && !m.WriteOnlySet.Equal(expectedSet) { + return diag.NewAttributeErrorDiagnostic( + path.Root("writeonly_set"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedSet, m.WriteOnlySet), + ) + } + + // Nested write-only attribute + expectedListObjType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "writeonly_string": types.StringType, + }, + } + expectedObjectType := map[string]attr.Type{ + "writeonly_int64": types.Int64Type, + "writeonly_bool": types.BoolType, + "writeonly_nested_list": types.ListType{ElemType: expectedListObjType}, + } + expectedObject := types.ObjectValueMust(expectedObjectType, map[string]attr.Value{ + "writeonly_int64": types.Int64Value(1234), + "writeonly_bool": types.BoolValue(true), + "writeonly_nested_list": types.ListValueMust(expectedListObjType, []attr.Value{ + types.ObjectValueMust(expectedListObjType.AttributeTypes(), map[string]attr.Value{ + "writeonly_string": types.StringValue("fakepassword1"), + }), + types.ObjectValueMust(expectedListObjType.AttributeTypes(), map[string]attr.Value{ + "writeonly_string": types.StringValue("fakepassword2"), + }), + }), + }) + + if !m.WriteOnlyNestedObject.IsNull() && !m.WriteOnlyNestedObject.Equal(expectedObject) { + return diag.NewAttributeErrorDiagnostic( + path.Root("writeonly_nested_object"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedObject, m.WriteOnlyNestedObject), + ) + } + + // Nested block with write-only attributes + expectedBlockObjectType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool_attr": types.BoolType, + "writeonly_bool": types.BoolType, + }, + } + expectedBlockListObjType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string_attr": types.StringType, + "writeonly_string": types.StringType, + "double_nested_object": expectedBlockObjectType, + }, + } + expectedListBlock := types.ListValueMust(expectedBlockListObjType, []attr.Value{ + types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ + "string_attr": types.StringValue("hello"), + "writeonly_string": types.StringValue("fakepassword1"), + "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ + "bool_attr": types.BoolValue(true), + "writeonly_bool": types.BoolValue(false), + }), + }), + types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ + "string_attr": types.StringValue("world"), + "writeonly_string": types.StringValue("fakepassword2"), + "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ + "bool_attr": types.BoolValue(false), + "writeonly_bool": types.BoolValue(true), + }), + }), + }) + + if len(m.NestedBlockList.Elements()) > 0 && !m.NestedBlockList.Equal(expectedListBlock) { + return diag.NewAttributeErrorDiagnostic( + path.Root("nested_block_list"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedListBlock, m.NestedBlockList), + ) + } + + return nil +} diff --git a/internal/framework6provider/writeonly_resource_test.go b/internal/framework6provider/writeonly_resource_test.go new file mode 100644 index 0000000..b4577ee --- /dev/null +++ b/internal/framework6provider/writeonly_resource_test.go @@ -0,0 +1,259 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself, to verify the config data is passed to the resource Create function. +// The state check assertions cannot be used for testing this data as write-only data should never be persisted in state. + +func TestWriteOnlyResource_CustomType(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_custom_ipv6 = "::" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestWriteOnlyResource_Primitive(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestWriteOnlyResource_Collection(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_set = ["fake", "password"] + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestWriteOnlyResource_NestedAttribute(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_nested_object = { + writeonly_int64 = 1234 + writeonly_bool = true + + writeonly_nested_list = [ + { + writeonly_string = "fakepassword1" + }, + { + writeonly_string = "fakepassword2" + } + ] + } + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), + }, + }, + }, + }) +} + +func TestWriteOnlyResource_NestedBlock(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + nested_block_list { + string_attr = "hello" + writeonly_string = "fakepassword1" + + double_nested_object { + bool_attr = true + writeonly_bool = false + } + } + nested_block_list { + string_attr = "world" + writeonly_string = "fakepassword2" + + double_nested_object { + bool_attr = false + writeonly_bool = true + } + } + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, + }, + }) +} From 4d3ff450d8c912b869365af7b964ae15a8978291 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 18 Dec 2024 09:05:34 -0500 Subject: [PATCH 04/35] simplify w/o tests --- go.mod | 2 +- go.sum | 4 +- .../framework5provider/writeonly_resource.go | 21 ++- .../writeonly_resource_test.go | 103 +------------ .../framework6provider/writeonly_resource.go | 27 ++-- .../writeonly_resource_test.go | 143 +----------------- 6 files changed, 32 insertions(+), 268 deletions(-) diff --git a/go.mod b/go.mod index 18e249b..6203d1b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de - github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241217224307-20550b9f8361 github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/zclconf/go-cty v1.15.1 ) diff --git a/go.sum b/go.sum index 31ecfcd..9af67b1 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de h1:qjSTbKECIWAEO5hBGqabykC/xwrAYIrD7fKSVKldFhY= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de/go.mod h1:To/3Wdozxp5QLvgcR4lKOdfiBcG70eoFZepcXro+nPQ= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9 h1:6EmxSSs3nMfNexYE6yS5M2ObXO+bULz8Pcq4W0O0KzA= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241126000311-1c24a16db1d9/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241217224307-20550b9f8361 h1:LNEW+gL06R3V8KaFOUXm5/5jz6POToplR9ql83rVkKo= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241217224307-20550b9f8361/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= diff --git a/internal/framework5provider/writeonly_resource.go b/internal/framework5provider/writeonly_resource.go index 85ac5da..4dc204a 100644 --- a/internal/framework5provider/writeonly_resource.go +++ b/internal/framework5provider/writeonly_resource.go @@ -33,16 +33,16 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "writeonly_custom_ipv6": schema.StringAttribute{ - Optional: true, + Required: true, WriteOnly: true, CustomType: iptypes.IPv6AddressType{}, }, "writeonly_string": schema.StringAttribute{ - Optional: true, + Required: true, WriteOnly: true, }, "writeonly_set": schema.SetAttribute{ - Optional: true, + Required: true, WriteOnly: true, ElementType: types.StringType, }, @@ -134,30 +134,29 @@ type WriteOnlyResourceModel struct { } // VerifyWriteOnlyData compares the hardcoded test data for the write-only attributes in this resource, raising -// error diagnostics if the data differs from expectations. Null write-only values will be ignored as all write-only -// attributes are optional +// error diagnostics if the data differs from expectations. func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { // Primitive write-only attributes expectedCustomIPv6 := "::" expectedString := "fakepassword" - if !m.WriteOnlyCustomIPv6.IsNull() && m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { + if m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { return diag.NewAttributeErrorDiagnostic( path.Root("writeonly_custom_ipv6"), "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6.ValueString()), + fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6), ) } - if !m.WriteOnlyString.IsNull() && m.WriteOnlyString.ValueString() != expectedString { + if m.WriteOnlyString.ValueString() != expectedString { return diag.NewAttributeErrorDiagnostic( path.Root("writeonly_string"), "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString.ValueString()), + fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString), ) } // Collection write-only attribute expectedSet := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}) - if !m.WriteOnlySet.IsNull() && !m.WriteOnlySet.Equal(expectedSet) { + if !m.WriteOnlySet.Equal(expectedSet) { return diag.NewAttributeErrorDiagnostic( path.Root("writeonly_set"), "Unexpected WriteOnly Value", @@ -198,7 +197,7 @@ func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { }), }) - if len(m.NestedBlockList.Elements()) > 0 && !m.NestedBlockList.Equal(expectedListBlock) { + if !m.NestedBlockList.Equal(expectedListBlock) { return diag.NewAttributeErrorDiagnostic( path.Root("nested_block_list"), "Unexpected WriteOnly Value", diff --git a/internal/framework5provider/writeonly_resource_test.go b/internal/framework5provider/writeonly_resource_test.go index 0f2c6ef..56150b5 100644 --- a/internal/framework5provider/writeonly_resource_test.go +++ b/internal/framework5provider/writeonly_resource_test.go @@ -17,10 +17,9 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself, to verify the config data is passed to the resource Create function. -// The state check assertions cannot be used for testing this data as write-only data should never be persisted in state. - -func TestWriteOnlyResource_CustomType(t *testing.T) { +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +func TestWriteOnlyResource(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Write-only attributes are only available in 1.11.0+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ @@ -33,104 +32,8 @@ func TestWriteOnlyResource_CustomType(t *testing.T) { { Config: `resource "framework_writeonly" "test" { writeonly_custom_ipv6 = "::" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - }, - }) -} - -func TestWriteOnlyResource_Primitive(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "framework": providerserver.NewProtocol5WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly" "test" { writeonly_string = "fakepassword" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - }, - }) -} - -func TestWriteOnlyResource_Collection(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "framework": providerserver.NewProtocol5WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly" "test" { writeonly_set = ["fake", "password"] - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - }, - }) -} - -func TestWriteOnlyResource_NestedBlock(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "framework": providerserver.NewProtocol5WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly" "test" { nested_block_list { string_attr = "hello" writeonly_string = "fakepassword1" diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go index 705381e..024ae0c 100644 --- a/internal/framework6provider/writeonly_resource.go +++ b/internal/framework6provider/writeonly_resource.go @@ -33,16 +33,16 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "writeonly_custom_ipv6": schema.StringAttribute{ - Optional: true, + Required: true, WriteOnly: true, CustomType: iptypes.IPv6AddressType{}, }, "writeonly_string": schema.StringAttribute{ - Optional: true, + Required: true, WriteOnly: true, }, "writeonly_set": schema.SetAttribute{ - Optional: true, + Required: true, WriteOnly: true, ElementType: types.StringType, }, @@ -51,7 +51,7 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r // - https://hashicorp.slack.com/archives/C071HC4JJCC/p1734465766242319?thread_ts=1734465749.748579&cid=C071HC4JJCC // // "nested_object": schema.SingleNestedAttribute{ - // Optional: true, + // Required: true, // Attributes: map[string]schema.Attribute{ // "string_attr": schema.StringAttribute{ // Required: true, @@ -63,7 +63,7 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r // }, // }, "writeonly_nested_object": schema.SingleNestedAttribute{ - Optional: true, + Required: true, WriteOnly: true, Attributes: map[string]schema.Attribute{ "writeonly_int64": schema.Int64Attribute{ @@ -177,30 +177,29 @@ type WriteOnlyResourceModel struct { } // VerifyWriteOnlyData compares the hardcoded test data for the write-only attributes in this resource, raising -// error diagnostics if the data differs from expectations. Null write-only values will be ignored as all write-only -// attributes are optional +// error diagnostics if the data differs from expectations. func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { // Primitive write-only attributes expectedCustomIPv6 := "::" expectedString := "fakepassword" - if !m.WriteOnlyCustomIPv6.IsNull() && m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { + if m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { return diag.NewAttributeErrorDiagnostic( path.Root("writeonly_custom_ipv6"), "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6.ValueString()), + fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6), ) } - if !m.WriteOnlyString.IsNull() && m.WriteOnlyString.ValueString() != expectedString { + if m.WriteOnlyString.ValueString() != expectedString { return diag.NewAttributeErrorDiagnostic( path.Root("writeonly_string"), "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString.ValueString()), + fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString), ) } // Collection write-only attribute expectedSet := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}) - if !m.WriteOnlySet.IsNull() && !m.WriteOnlySet.Equal(expectedSet) { + if !m.WriteOnlySet.Equal(expectedSet) { return diag.NewAttributeErrorDiagnostic( path.Root("writeonly_set"), "Unexpected WriteOnly Value", @@ -232,7 +231,7 @@ func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { }), }) - if !m.WriteOnlyNestedObject.IsNull() && !m.WriteOnlyNestedObject.Equal(expectedObject) { + if !m.WriteOnlyNestedObject.Equal(expectedObject) { return diag.NewAttributeErrorDiagnostic( path.Root("writeonly_nested_object"), "Unexpected WriteOnly Value", @@ -273,7 +272,7 @@ func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { }), }) - if len(m.NestedBlockList.Elements()) > 0 && !m.NestedBlockList.Equal(expectedListBlock) { + if !m.NestedBlockList.Equal(expectedListBlock) { return diag.NewAttributeErrorDiagnostic( path.Root("nested_block_list"), "Unexpected WriteOnly Value", diff --git a/internal/framework6provider/writeonly_resource_test.go b/internal/framework6provider/writeonly_resource_test.go index b4577ee..a3f7cc1 100644 --- a/internal/framework6provider/writeonly_resource_test.go +++ b/internal/framework6provider/writeonly_resource_test.go @@ -17,10 +17,9 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself, to verify the config data is passed to the resource Create function. -// The state check assertions cannot be used for testing this data as write-only data should never be persisted in state. - -func TestWriteOnlyResource_CustomType(t *testing.T) { +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +func TestWriteOnlyResource(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Write-only attributes are only available in 1.11.0+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ @@ -33,110 +32,8 @@ func TestWriteOnlyResource_CustomType(t *testing.T) { { Config: `resource "framework_writeonly" "test" { writeonly_custom_ipv6 = "::" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - }, - }) -} - -func TestWriteOnlyResource_Primitive(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "framework": providerserver.NewProtocol6WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly" "test" { writeonly_string = "fakepassword" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - }, - }) -} - -func TestWriteOnlyResource_Collection(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "framework": providerserver.NewProtocol6WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly" "test" { writeonly_set = ["fake", "password"] - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - }, - }) -} - -func TestWriteOnlyResource_NestedAttribute(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "framework": providerserver.NewProtocol6WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly" "test" { writeonly_nested_object = { writeonly_int64 = 1234 writeonly_bool = true @@ -150,40 +47,6 @@ func TestWriteOnlyResource_NestedAttribute(t *testing.T) { } ] } - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), - statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListSizeExact(0)), - }, - }, - }, - }) -} - -func TestWriteOnlyResource_NestedBlock(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "framework": providerserver.NewProtocol6WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly" "test" { nested_block_list { string_attr = "hello" writeonly_string = "fakepassword1" From 2f731d979ba4f25aedbd067c6b132060efc22984 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 19 Dec 2024 08:25:59 -0500 Subject: [PATCH 05/35] some SDKv2 happy path tests --- go.mod | 12 +- go.sum | 20 +- .../framework5provider/writeonly_resource.go | 1 - .../framework6provider/writeonly_resource.go | 1 - internal/sdkv2provider/provider.go | 2 +- .../sdkv2provider/resource_user_write_only.go | 137 ------------- .../resource_user_write_only_test.go | 100 ---------- internal/sdkv2provider/resource_write_only.go | 181 ++++++++++++++++++ .../sdkv2provider/resource_write_only_test.go | 142 ++++++++++++++ 9 files changed, 340 insertions(+), 256 deletions(-) delete mode 100644 internal/sdkv2provider/resource_user_write_only.go delete mode 100644 internal/sdkv2provider/resource_user_write_only_test.go create mode 100644 internal/sdkv2provider/resource_write_only.go create mode 100644 internal/sdkv2provider/resource_write_only_test.go diff --git a/go.mod b/go.mod index 6203d1b..5cd5f4f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hashicorp/terraform-provider-corner go 1.22.7 require ( + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-memdb v1.3.4 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-json v0.23.0 @@ -12,7 +13,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de - github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241217224307-20550b9f8361 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241218185828-0816d60b3c19 github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/zclconf/go-cty v1.15.1 ) @@ -28,7 +29,6 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix v1.3.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -55,12 +55,12 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.29.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect diff --git a/go.sum b/go.sum index 9af67b1..015dad6 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de h1:qjSTbKECIWAEO5hBGqabykC/xwrAYIrD7fKSVKldFhY= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de/go.mod h1:To/3Wdozxp5QLvgcR4lKOdfiBcG70eoFZepcXro+nPQ= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241217224307-20550b9f8361 h1:LNEW+gL06R3V8KaFOUXm5/5jz6POToplR9ql83rVkKo= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241217224307-20550b9f8361/go.mod h1:zrpTQUSETV/RY4ipJKTSxluZOk7yCB5O9Lufq2uBd+k= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241218185828-0816d60b3c19 h1:ff0CD1ARqGkjlxZKdxDkJReOd+kXneF8okuG1Bj0//M= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241218185828-0816d60b3c19/go.mod h1:wsPZ9W0E/8tID4aejTrYrNPlJS8YhkEepeMeZlUaB8M= github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -165,8 +165,8 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= @@ -179,8 +179,8 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -193,8 +193,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -202,8 +202,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/framework5provider/writeonly_resource.go b/internal/framework5provider/writeonly_resource.go index 4dc204a..1aa8a11 100644 --- a/internal/framework5provider/writeonly_resource.go +++ b/internal/framework5provider/writeonly_resource.go @@ -90,7 +90,6 @@ func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateReques return } - // If the write-only data exists in config, verify the data is what we hardcoded in the resource resp.Diagnostics.Append(config.VerifyWriteOnlyData()) if resp.Diagnostics.HasError() { return diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go index 024ae0c..684ac31 100644 --- a/internal/framework6provider/writeonly_resource.go +++ b/internal/framework6provider/writeonly_resource.go @@ -132,7 +132,6 @@ func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateReques return } - // If the write-only data exists in config, verify the data is what we hardcoded in the resource resp.Diagnostics.Append(config.VerifyWriteOnlyData()) if resp.Diagnostics.HasError() { return diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index ec62c30..0eae168 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -27,7 +27,7 @@ func New() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ "corner_user": resourceUser(), - "corner_user_writeonly": resourceUserWriteOnly(), + "corner_writeonly": resourceWriteOnly(), "corner_bigint": resourceBigint(), "corner_user_cty": resourceUserCty(), "corner_deferred_action": resourceDeferredAction(), diff --git a/internal/sdkv2provider/resource_user_write_only.go b/internal/sdkv2provider/resource_user_write_only.go deleted file mode 100644 index 8f7cf47..0000000 --- a/internal/sdkv2provider/resource_user_write_only.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -//nolint:forcetypeassert // Test SDK provider -package sdkv2 - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-provider-corner/internal/backend" -) - -func resourceUserWriteOnly() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceUserWriteOnlyCreate, - ReadContext: resourceUserWriteOnlyRead, - UpdateContext: resourceUserWriteOnlyUpdate, - DeleteContext: resourceUserWriteOnlyDelete, - - Schema: map[string]*schema.Schema{ - "email": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "name": { - Type: schema.TypeString, - Required: true, - }, - "age": { - Type: schema.TypeInt, - Required: true, - }, - "password": { - Type: schema.TypeString, - Optional: true, - WriteOnly: true, - }, - "saved_password": { - Type: schema.TypeString, - Computed: true, - }, - }, - } -} - -func resourceUserWriteOnlyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*backend.Client) - newUser := &backend.User{ - Email: d.Get("email").(string), - Name: d.Get("name").(string), - Age: d.Get("age").(int), - } - - err := client.CreateUser(newUser) - if err != nil { - return diag.FromErr(err) - } - - password := d.Get("password").(string) - - err = d.Set("password", password) - if err != nil { - return diag.FromErr(err) - } - err = d.Set("saved_password", password) - if err != nil { - return diag.FromErr(err) - } - - return resourceUserWriteOnlyRead(ctx, d, meta) -} - -func resourceUserWriteOnlyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*backend.Client) - - email := d.Get("email").(string) - - p, err := client.ReadUser(email) - if err != nil { - return diag.FromErr(err) - } - - if p == nil { - return nil - } - - d.SetId(email) - - err = d.Set("name", p.Name) - if err != nil { - return diag.FromErr(err) - } - err = d.Set("age", p.Age) - if err != nil { - return diag.FromErr(err) - } - - return nil -} - -func resourceUserWriteOnlyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*backend.Client) - - user := &backend.User{ - Email: d.Get("email").(string), - Name: d.Get("name").(string), - Age: d.Get("age").(int), - } - - err := client.UpdateUser(user) - if err != nil { - return diag.FromErr(err) - } - - return nil -} - -func resourceUserWriteOnlyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*backend.Client) - - user := &backend.User{ - Email: d.Get("email").(string), - Name: d.Get("name").(string), - Age: d.Get("age").(int), - } - - err := client.DeleteUser(user) - if err != nil { - return diag.FromErr(err) - } - - return nil -} diff --git a/internal/sdkv2provider/resource_user_write_only_test.go b/internal/sdkv2provider/resource_user_write_only_test.go deleted file mode 100644 index 534a0a9..0000000 --- a/internal/sdkv2provider/resource_user_write_only_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package sdkv2 - -import ( - "testing" - - "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-plugin-framework/providerserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/statecheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" - - framework "github.com/hashicorp/terraform-provider-corner/internal/framework5provider" -) - -func TestSchemaWriteOnly_basic(t *testing.T) { - t.Parallel() - - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11 and later - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "framework": providerserver.NewProtocol5WithError(framework.New()), - }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: `ephemeral "framework_schema" "test" { - string_attribute = "test" - } - - resource "corner_user_writeonly" "foo" { - email = "ford@prefect.co" - name = "Ford Prefect" - age = 200 - password = ephemeral.framework_schema.test.string_attribute - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), - plancheck.ExpectUnknownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password")), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), - statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password"), knownvalue.NotNull()), - }, - }, - }, - }) -} - -func TestSchemaWriteOnly_randomPassword(t *testing.T) { - t.Parallel() - - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11 and later - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), - }, - ExternalProviders: map[string]resource.ExternalProvider{ - "random": { - Source: "registry.terraform.io/hashicorp/random", - }, - }, - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "framework": providerserver.NewProtocol5WithError(framework.New()), - }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: `ephemeral "random_password" "test" { - length = 20 - } - - resource "corner_user_writeonly" "foo" { - email = "ford@prefect.co" - name = "Ford Prefect" - age = 200 - password = ephemeral.random_password.test.result - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), - plancheck.ExpectUnknownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password")), - }, - }, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("password"), knownvalue.Null()), - statecheck.ExpectKnownValue("corner_user_writeonly.foo", tfjsonpath.New("saved_password"), knownvalue.NotNull()), - }, - }, - }, - }) -} diff --git a/internal/sdkv2provider/resource_write_only.go b/internal/sdkv2provider/resource_write_only.go new file mode 100644 index 0000000..7a1611e --- /dev/null +++ b/internal/sdkv2provider/resource_write_only.go @@ -0,0 +1,181 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//nolint:forcetypeassert // Test SDK provider +package sdkv2 + +import ( + "context" + "errors" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceWriteOnly() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceWriteOnlyCreate, + ReadContext: resourceWriteOnlyRead, + UpdateContext: resourceWriteOnlyUpdate, + DeleteContext: resourceWriteOnlyDelete, + + Schema: map[string]*schema.Schema{ + "string_attr": { + Type: schema.TypeString, + Required: true, + }, + "writeonly_bool": { + Type: schema.TypeBool, + Required: true, + WriteOnly: true, + }, + "writeonly_string": { + Type: schema.TypeString, + Required: true, + WriteOnly: true, + }, + "writeonly_int": { + Type: schema.TypeInt, + Required: true, + WriteOnly: true, + }, + "nested_list_block": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "string_attr": { + Type: schema.TypeString, + Required: true, + }, + "opt_or_computed_string_attr": { + Type: schema.TypeString, + Default: "computed value!", + Optional: true, + }, + "writeonly_string": { + Type: schema.TypeString, + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } +} + +func resourceWriteOnlyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId("fakeid-123") + + return verifyWriteOnlyData(d) +} + +func resourceWriteOnlyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Config isn't set for Read, so can't verify write-only data + return nil +} + +func resourceWriteOnlyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return verifyWriteOnlyData(d) +} + +func resourceWriteOnlyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Config isn't set for Delete, so can't verify write-only data + return nil +} + +// verifyWriteOnlyData compares the hardcoded test data for the write-only attributes in this resource, raising +// error diagnostics if the data differs from expectations. +func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { + // Write-only string assert + strVal, diags := d.GetRawConfigAt(cty.GetAttrPath("writeonly_string")) + if diags.HasError() { + return diags + } + if strVal.IsNull() { + return diag.FromErr(errors.New("expected `writeonly_string` write-only attribute to be set, got: ")) + } + expectedString := "fakepassword" + if strVal.AsString() != expectedString { + return diag.Errorf("expected `writeonly_string` to be: %q, got: %q", expectedString, strVal.AsString()) + } + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err := d.Set("writeonly_string", "different value") + if err != nil { + return diag.FromErr(err) + } + + // Write-only bool assert + boolVal, diags := d.GetRawConfigAt(cty.GetAttrPath("writeonly_bool")) + if diags.HasError() { + return diags + } + if boolVal.IsNull() { + return diag.FromErr(errors.New("expected `writeonly_bool` write-only attribute to be set, got: ")) + } + if boolVal.False() { + return diag.FromErr(errors.New("expected `writeonly_bool` to be: true, got: false")) + } + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err = d.Set("writeonly_bool", false) + if err != nil { + return diag.FromErr(err) + } + + // Write-only int assert + intVal, diags := d.GetRawConfigAt(cty.GetAttrPath("writeonly_int")) + if diags.HasError() { + return diags + } + if intVal.IsNull() { + return diag.FromErr(errors.New("expected `writeonly_int` write-only attribute to be set, got: ")) + } + expectedInt := int64(1234) + gotInt, _ := intVal.AsBigFloat().Int64() + if gotInt != expectedInt { + return diag.Errorf("expected `writeonly_int` to be: %d, got: %d", expectedInt, gotInt) + } + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err = d.Set("writeonly_int", 999) + if err != nil { + return diag.FromErr(err) + } + + // Nested list block with write-only attribute + listBlockVal, diags := d.GetRawConfigAt(cty.GetAttrPath("nested_list_block")) + if diags.HasError() { + return diags + } + + if listBlockVal.IsNull() || listBlockVal.LengthInt() != 1 { + return diag.Errorf("expected `nested_list_block` to have length of 1, got: %s", listBlockVal.GoString()) + } + + nestedWriteOnlyStr, diags := d.GetRawConfigAt(cty.GetAttrPath("nested_list_block").IndexInt(0).GetAttr("writeonly_string")) + if diags.HasError() { + return diags + } + expectedNestedWriteOnlyStr := "fakepassword" + if nestedWriteOnlyStr.AsString() != expectedNestedWriteOnlyStr { + return diag.Errorf("expected `nested_list_block.0.writeonly_string` to be: %s, got: %s", expectedNestedWriteOnlyStr, nestedWriteOnlyStr.AsString()) + } + err = d.Set("nested_list_block", []map[string]any{ + { + "string_attr": d.Get("nested_list_block.0.string_attr"), + "opt_or_computed_string_attr": d.Get("nested_list_block.0.opt_or_computed_string_attr"), + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + "writeonly_string": "different value!", + }, + }) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/internal/sdkv2provider/resource_write_only_test.go b/internal/sdkv2provider/resource_write_only_test.go new file mode 100644 index 0000000..80ba90a --- /dev/null +++ b/internal/sdkv2provider/resource_write_only_test.go @@ -0,0 +1,142 @@ +package sdkv2 + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create/Update functions. +func TestWriteOnlyResource(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly" "test" { + string_attr = "hello!" + writeonly_bool = true + writeonly_string = "fakepassword" + writeonly_int = 1234 + nested_list_block { + string_attr = "hello!" + writeonly_string = "fakepassword" + } + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("string_attr"), knownvalue.StringExact("hello!")), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("hello!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("computed value!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("string_attr"), knownvalue.StringExact("hello!")), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("hello!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("computed value!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), + }, + }, + { + Config: `resource "corner_writeonly" "test" { + string_attr = "world!" + writeonly_bool = true + writeonly_string = "fakepassword" + writeonly_int = 1234 + nested_list_block { + string_attr = "world!" + opt_or_computed_string_attr = "config value!" + writeonly_string = "fakepassword" + } + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("string_attr"), knownvalue.StringExact("world!")), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("world!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("config value!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("string_attr"), knownvalue.StringExact("world!")), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("world!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("config value!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), + }, + }, + }, + }) +} From 5fe00473550d5ad77c40e3cb5e3173145626fa01 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 19 Dec 2024 10:35:35 -0500 Subject: [PATCH 06/35] add double nested set --- internal/sdkv2provider/resource_write_only.go | 74 ++++++++++++++++++- .../sdkv2provider/resource_write_only_test.go | 69 +++++++++++++++++ 2 files changed, 139 insertions(+), 4 deletions(-) diff --git a/internal/sdkv2provider/resource_write_only.go b/internal/sdkv2provider/resource_write_only.go index 7a1611e..4c2be9b 100644 --- a/internal/sdkv2provider/resource_write_only.go +++ b/internal/sdkv2provider/resource_write_only.go @@ -15,6 +15,10 @@ import ( func resourceWriteOnly() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceWriteOnlyCreate, ReadContext: resourceWriteOnlyRead, UpdateContext: resourceWriteOnlyUpdate, @@ -50,15 +54,43 @@ func resourceWriteOnly() *schema.Resource { Required: true, }, "opt_or_computed_string_attr": { - Type: schema.TypeString, - Default: "computed value!", + Type: schema.TypeString, + DefaultFunc: func() (interface{}, error) { + return "computed value!", nil + }, Optional: true, + Computed: true, }, "writeonly_string": { Type: schema.TypeString, Required: true, WriteOnly: true, }, + "double_nested_set_block": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "string_attr": { + Type: schema.TypeString, + Required: true, + }, + "opt_or_computed_string_attr": { + Type: schema.TypeString, + DefaultFunc: func() (interface{}, error) { + return "computed value!", nil + }, + Optional: true, + Computed: true, + }, + "writeonly_string": { + Type: schema.TypeString, + Required: true, + WriteOnly: true, + }, + }, + }, + }, }, }, }, @@ -147,7 +179,8 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { } // Nested list block with write-only attribute - listBlockVal, diags := d.GetRawConfigAt(cty.GetAttrPath("nested_list_block")) + listBlockPath := cty.GetAttrPath("nested_list_block") + listBlockVal, diags := d.GetRawConfigAt(listBlockPath) if diags.HasError() { return diags } @@ -156,7 +189,7 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { return diag.Errorf("expected `nested_list_block` to have length of 1, got: %s", listBlockVal.GoString()) } - nestedWriteOnlyStr, diags := d.GetRawConfigAt(cty.GetAttrPath("nested_list_block").IndexInt(0).GetAttr("writeonly_string")) + nestedWriteOnlyStr, diags := d.GetRawConfigAt(listBlockPath.IndexInt(0).GetAttr("writeonly_string")) if diags.HasError() { return diags } @@ -164,6 +197,30 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { if nestedWriteOnlyStr.AsString() != expectedNestedWriteOnlyStr { return diag.Errorf("expected `nested_list_block.0.writeonly_string` to be: %s, got: %s", expectedNestedWriteOnlyStr, nestedWriteOnlyStr.AsString()) } + + // Double nested set block with write-only attribute + setBlockPath := cty.GetAttrPath("nested_list_block").IndexInt(0).GetAttr("double_nested_set_block") + setBlockVal, diags := d.GetRawConfigAt(setBlockPath) + if diags.HasError() { + return diags + } + + if setBlockVal.IsNull() || setBlockVal.LengthInt() != 1 { + return diag.Errorf("expected `nested_list_block.0.double_nested_set_block` to have length of 1, got: %s", setBlockVal.GoString()) + } + + setSlice := setBlockVal.AsValueSlice() + + doubleNestedWriteOnlyStr := setSlice[0].GetAttr("writeonly_string") + if diags.HasError() { + return diags + } + expecteDoubleNestedWriteOnlyStr := "fakepassword" + if doubleNestedWriteOnlyStr.AsString() != expecteDoubleNestedWriteOnlyStr { + return diag.Errorf("expected `nested_list_block.0.double_nested_set_block.0.writeonly_string` to be: %s, got: %s", expecteDoubleNestedWriteOnlyStr, doubleNestedWriteOnlyStr.AsString()) + } + + // We can only set the root list, so this function also grabs data from ResourceData to ensure we use computed/default data as well err = d.Set("nested_list_block", []map[string]any{ { "string_attr": d.Get("nested_list_block.0.string_attr"), @@ -171,6 +228,15 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { // Setting shouldn't result in anything sent back to Terraform, but we want to test that // our SDKv2 logic would revert these changes. "writeonly_string": "different value!", + "double_nested_set_block": []map[string]any{ + { + "string_attr": d.Get("nested_list_block.0.double_nested_set_block").(*schema.Set).List()[0].(map[string]any)["string_attr"], + "opt_or_computed_string_attr": d.Get("nested_list_block.0.double_nested_set_block").(*schema.Set).List()[0].(map[string]any)["opt_or_computed_string_attr"], + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + "writeonly_string": "different value!", + }, + }, }, }) if err != nil { diff --git a/internal/sdkv2provider/resource_write_only_test.go b/internal/sdkv2provider/resource_write_only_test.go index 80ba90a..93a5b81 100644 --- a/internal/sdkv2provider/resource_write_only_test.go +++ b/internal/sdkv2provider/resource_write_only_test.go @@ -33,6 +33,10 @@ func TestWriteOnlyResource(t *testing.T) { nested_list_block { string_attr = "hello!" writeonly_string = "fakepassword" + double_nested_set_block { + string_attr = "hello!" + writeonly_string = "fakepassword" + } } }`, ConfigPlanChecks: resource.ConfigPlanChecks{ @@ -56,6 +60,21 @@ func TestWriteOnlyResource(t *testing.T) { tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), knownvalue.Null(), ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("hello!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("computed value!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), }, }, ConfigStateChecks: []statecheck.StateCheck{ @@ -78,6 +97,21 @@ func TestWriteOnlyResource(t *testing.T) { tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), knownvalue.Null(), ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("hello!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("computed value!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), }, }, { @@ -90,6 +124,11 @@ func TestWriteOnlyResource(t *testing.T) { string_attr = "world!" opt_or_computed_string_attr = "config value!" writeonly_string = "fakepassword" + double_nested_set_block { + string_attr = "world!" + opt_or_computed_string_attr = "config value!" + writeonly_string = "fakepassword" + } } }`, ConfigPlanChecks: resource.ConfigPlanChecks{ @@ -113,6 +152,21 @@ func TestWriteOnlyResource(t *testing.T) { tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), knownvalue.Null(), ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("world!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("config value!"), + ), + plancheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), }, }, ConfigStateChecks: []statecheck.StateCheck{ @@ -135,6 +189,21 @@ func TestWriteOnlyResource(t *testing.T) { tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), knownvalue.Null(), ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), + knownvalue.StringExact("world!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), + knownvalue.StringExact("config value!"), + ), + statecheck.ExpectKnownValue( + "corner_writeonly.test", + tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), + knownvalue.Null(), + ), }, }, }, From c5a6557130057286428d446125638d7b60908d3b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 19 Dec 2024 10:35:58 -0500 Subject: [PATCH 07/35] enforce data consistency on sdkv2 tests --- internal/sdkv2provider/data_source_bigint.go | 3 +++ internal/sdkv2provider/data_source_regions.go | 3 +++ internal/sdkv2provider/data_source_regions_cty.go | 3 +++ internal/sdkv2provider/resource_bigint.go | 4 ++++ internal/sdkv2provider/resource_deferred_action.go | 4 ++++ internal/sdkv2provider/resource_user.go | 4 ++++ internal/sdkv2provider/resource_user_cty.go | 4 ++++ internal/sdkv2testingprovider/data_source_bigint.go | 3 +++ internal/sdkv2testingprovider/data_source_regions.go | 3 +++ internal/sdkv2testingprovider/data_source_regions_cty.go | 4 ++++ internal/sdkv2testingprovider/resource_bigint.go | 4 ++++ internal/sdkv2testingprovider/resource_user.go | 4 ++++ internal/sdkv2testingprovider/resource_user_cty.go | 4 ++++ 13 files changed, 47 insertions(+) diff --git a/internal/sdkv2provider/data_source_bigint.go b/internal/sdkv2provider/data_source_bigint.go index e9d0fba..7445dc3 100644 --- a/internal/sdkv2provider/data_source_bigint.go +++ b/internal/sdkv2provider/data_source_bigint.go @@ -12,6 +12,9 @@ import ( func dataSourceBigint() *schema.Resource { return &schema.Resource{ + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + ReadContext: dataSourceBigintRead, Schema: map[string]*schema.Schema{ diff --git a/internal/sdkv2provider/data_source_regions.go b/internal/sdkv2provider/data_source_regions.go index ba3d031..8ac9926 100644 --- a/internal/sdkv2provider/data_source_regions.go +++ b/internal/sdkv2provider/data_source_regions.go @@ -14,6 +14,9 @@ import ( func dataSourceRegions() *schema.Resource { return &schema.Resource{ + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + ReadContext: dataSourceRegionsRead, Schema: map[string]*schema.Schema{ diff --git a/internal/sdkv2provider/data_source_regions_cty.go b/internal/sdkv2provider/data_source_regions_cty.go index e428feb..e2d6a47 100644 --- a/internal/sdkv2provider/data_source_regions_cty.go +++ b/internal/sdkv2provider/data_source_regions_cty.go @@ -15,6 +15,9 @@ import ( func dataSourceRegionsCty() *schema.Resource { return &schema.Resource{ + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + ReadContext: dataSourceRegionsCtyRead, Schema: map[string]*schema.Schema{ diff --git a/internal/sdkv2provider/resource_bigint.go b/internal/sdkv2provider/resource_bigint.go index c0fe94d..c1cd6e3 100644 --- a/internal/sdkv2provider/resource_bigint.go +++ b/internal/sdkv2provider/resource_bigint.go @@ -14,6 +14,10 @@ import ( func resourceBigint() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceBigintCreate, ReadContext: resourceBigintRead, UpdateContext: resourceBigintUpdate, diff --git a/internal/sdkv2provider/resource_deferred_action.go b/internal/sdkv2provider/resource_deferred_action.go index 5785f61..3281df8 100644 --- a/internal/sdkv2provider/resource_deferred_action.go +++ b/internal/sdkv2provider/resource_deferred_action.go @@ -17,6 +17,10 @@ import ( func resourceDeferredAction() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceDeferredActionCreate, ReadContext: resourceDeferredActionRead, UpdateContext: resourceDeferredActionUpdate, diff --git a/internal/sdkv2provider/resource_user.go b/internal/sdkv2provider/resource_user.go index 393b19d..75f86f7 100644 --- a/internal/sdkv2provider/resource_user.go +++ b/internal/sdkv2provider/resource_user.go @@ -14,6 +14,10 @@ import ( func resourceUser() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceUserCreate, ReadContext: resourceUserRead, UpdateContext: resourceUserUpdate, diff --git a/internal/sdkv2provider/resource_user_cty.go b/internal/sdkv2provider/resource_user_cty.go index cebe096..22707a6 100644 --- a/internal/sdkv2provider/resource_user_cty.go +++ b/internal/sdkv2provider/resource_user_cty.go @@ -16,6 +16,10 @@ import ( func resourceUserCty() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceUserCtyCreate, ReadContext: resourceUserCtyRead, UpdateContext: resourceUserCtyUpdate, diff --git a/internal/sdkv2testingprovider/data_source_bigint.go b/internal/sdkv2testingprovider/data_source_bigint.go index 6e86660..d57e857 100644 --- a/internal/sdkv2testingprovider/data_source_bigint.go +++ b/internal/sdkv2testingprovider/data_source_bigint.go @@ -12,6 +12,9 @@ import ( func dataSourceBigint() *schema.Resource { return &schema.Resource{ + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + ReadContext: dataSourceBigintRead, Schema: map[string]*schema.Schema{ diff --git a/internal/sdkv2testingprovider/data_source_regions.go b/internal/sdkv2testingprovider/data_source_regions.go index 9fc750c..740331d 100644 --- a/internal/sdkv2testingprovider/data_source_regions.go +++ b/internal/sdkv2testingprovider/data_source_regions.go @@ -14,6 +14,9 @@ import ( func dataSourceRegions() *schema.Resource { return &schema.Resource{ + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + ReadContext: dataSourceRegionsRead, Schema: map[string]*schema.Schema{ diff --git a/internal/sdkv2testingprovider/data_source_regions_cty.go b/internal/sdkv2testingprovider/data_source_regions_cty.go index f15456b..765374b 100644 --- a/internal/sdkv2testingprovider/data_source_regions_cty.go +++ b/internal/sdkv2testingprovider/data_source_regions_cty.go @@ -15,6 +15,10 @@ import ( func dataSourceRegionsCty() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + ReadContext: dataSourceRegionsCtyRead, Schema: map[string]*schema.Schema{ diff --git a/internal/sdkv2testingprovider/resource_bigint.go b/internal/sdkv2testingprovider/resource_bigint.go index 6a05faf..ebc6cd4 100644 --- a/internal/sdkv2testingprovider/resource_bigint.go +++ b/internal/sdkv2testingprovider/resource_bigint.go @@ -14,6 +14,10 @@ import ( func resourceBigint() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceBigintCreate, ReadContext: resourceBigintRead, UpdateContext: resourceBigintUpdate, diff --git a/internal/sdkv2testingprovider/resource_user.go b/internal/sdkv2testingprovider/resource_user.go index f29896a..161f596 100644 --- a/internal/sdkv2testingprovider/resource_user.go +++ b/internal/sdkv2testingprovider/resource_user.go @@ -14,6 +14,10 @@ import ( func resourceUser() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceUserCreate, ReadContext: resourceUserRead, UpdateContext: resourceUserUpdate, diff --git a/internal/sdkv2testingprovider/resource_user_cty.go b/internal/sdkv2testingprovider/resource_user_cty.go index 6f9f2e5..9d46be7 100644 --- a/internal/sdkv2testingprovider/resource_user_cty.go +++ b/internal/sdkv2testingprovider/resource_user_cty.go @@ -16,6 +16,10 @@ import ( func resourceUserCty() *schema.Resource { return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + CreateContext: resourceUserCtyCreate, ReadContext: resourceUserCtyRead, UpdateContext: resourceUserCtyUpdate, From 0e9d2de04e327b5d00ce357b856982f7179b3714 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 19 Dec 2024 18:35:48 -0500 Subject: [PATCH 08/35] write only negative tests for SDKv2 --- go.mod | 2 +- go.sum | 4 +- .../writeonly_resource_test.go | 2 + .../writeonly_resource_test.go | 2 + internal/sdkv2provider/resource_write_only.go | 100 +++++++++--------- .../sdkv2provider/resource_write_only_test.go | 77 ++++++++++++++ 6 files changed, 133 insertions(+), 54 deletions(-) diff --git a/go.mod b/go.mod index 5cd5f4f..74696fb 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de - github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241218185828-0816d60b3c19 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1 github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/zclconf/go-cty v1.15.1 ) diff --git a/go.sum b/go.sum index 015dad6..90b84bb 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de h1:qjSTbKECIWAEO5hBGqabykC/xwrAYIrD7fKSVKldFhY= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de/go.mod h1:To/3Wdozxp5QLvgcR4lKOdfiBcG70eoFZepcXro+nPQ= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241218185828-0816d60b3c19 h1:ff0CD1ARqGkjlxZKdxDkJReOd+kXneF8okuG1Bj0//M= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241218185828-0816d60b3c19/go.mod h1:wsPZ9W0E/8tID4aejTrYrNPlJS8YhkEepeMeZlUaB8M= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1 h1:pUl5Z8PQWNDz/VGIl8RGaSATG+Ja7W31+u6jJYRtiPw= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1/go.mod h1:wsPZ9W0E/8tID4aejTrYrNPlJS8YhkEepeMeZlUaB8M= github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= diff --git a/internal/framework5provider/writeonly_resource_test.go b/internal/framework5provider/writeonly_resource_test.go index 56150b5..d46f0f9 100644 --- a/internal/framework5provider/writeonly_resource_test.go +++ b/internal/framework5provider/writeonly_resource_test.go @@ -105,3 +105,5 @@ func TestWriteOnlyResource(t *testing.T) { }, }) } + +// TODO: add negative tests for old terraform versions diff --git a/internal/framework6provider/writeonly_resource_test.go b/internal/framework6provider/writeonly_resource_test.go index a3f7cc1..0e54976 100644 --- a/internal/framework6provider/writeonly_resource_test.go +++ b/internal/framework6provider/writeonly_resource_test.go @@ -120,3 +120,5 @@ func TestWriteOnlyResource(t *testing.T) { }, }) } + +// TODO: add negative tests for old terraform versions diff --git a/internal/sdkv2provider/resource_write_only.go b/internal/sdkv2provider/resource_write_only.go index 4c2be9b..bb1d2b3 100644 --- a/internal/sdkv2provider/resource_write_only.go +++ b/internal/sdkv2provider/resource_write_only.go @@ -31,17 +31,17 @@ func resourceWriteOnly() *schema.Resource { }, "writeonly_bool": { Type: schema.TypeBool, - Required: true, + Optional: true, WriteOnly: true, }, "writeonly_string": { Type: schema.TypeString, - Required: true, + Optional: true, WriteOnly: true, }, "writeonly_int": { Type: schema.TypeInt, - Required: true, + Optional: true, WriteOnly: true, }, "nested_list_block": { @@ -63,7 +63,7 @@ func resourceWriteOnly() *schema.Resource { }, "writeonly_string": { Type: schema.TypeString, - Required: true, + Optional: true, WriteOnly: true, }, "double_nested_set_block": { @@ -85,7 +85,7 @@ func resourceWriteOnly() *schema.Resource { }, "writeonly_string": { Type: schema.TypeString, - Required: true, + Optional: true, WriteOnly: true, }, }, @@ -126,18 +126,17 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { if diags.HasError() { return diags } - if strVal.IsNull() { - return diag.FromErr(errors.New("expected `writeonly_string` write-only attribute to be set, got: ")) - } - expectedString := "fakepassword" - if strVal.AsString() != expectedString { - return diag.Errorf("expected `writeonly_string` to be: %q, got: %q", expectedString, strVal.AsString()) - } - // Setting shouldn't result in anything sent back to Terraform, but we want to test that - // our SDKv2 logic would revert these changes. - err := d.Set("writeonly_string", "different value") - if err != nil { - return diag.FromErr(err) + if !strVal.IsNull() { + expectedString := "fakepassword" + if strVal.AsString() != expectedString { + return diag.Errorf("expected `writeonly_string` to be: %q, got: %q", expectedString, strVal.AsString()) + } + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err := d.Set("writeonly_string", "different value") + if err != nil { + return diag.FromErr(err) + } } // Write-only bool assert @@ -145,17 +144,16 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { if diags.HasError() { return diags } - if boolVal.IsNull() { - return diag.FromErr(errors.New("expected `writeonly_bool` write-only attribute to be set, got: ")) - } - if boolVal.False() { - return diag.FromErr(errors.New("expected `writeonly_bool` to be: true, got: false")) - } - // Setting shouldn't result in anything sent back to Terraform, but we want to test that - // our SDKv2 logic would revert these changes. - err = d.Set("writeonly_bool", false) - if err != nil { - return diag.FromErr(err) + if !boolVal.IsNull() { + if boolVal.False() { + return diag.FromErr(errors.New("expected `writeonly_bool` to be: true, got: false")) + } + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err := d.Set("writeonly_bool", false) + if err != nil { + return diag.FromErr(err) + } } // Write-only int assert @@ -163,19 +161,18 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { if diags.HasError() { return diags } - if intVal.IsNull() { - return diag.FromErr(errors.New("expected `writeonly_int` write-only attribute to be set, got: ")) - } - expectedInt := int64(1234) - gotInt, _ := intVal.AsBigFloat().Int64() - if gotInt != expectedInt { - return diag.Errorf("expected `writeonly_int` to be: %d, got: %d", expectedInt, gotInt) - } - // Setting shouldn't result in anything sent back to Terraform, but we want to test that - // our SDKv2 logic would revert these changes. - err = d.Set("writeonly_int", 999) - if err != nil { - return diag.FromErr(err) + if !intVal.IsNull() { + expectedInt := int64(1234) + gotInt, _ := intVal.AsBigFloat().Int64() + if gotInt != expectedInt { + return diag.Errorf("expected `writeonly_int` to be: %d, got: %d", expectedInt, gotInt) + } + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err := d.Set("writeonly_int", 999) + if err != nil { + return diag.FromErr(err) + } } // Nested list block with write-only attribute @@ -193,9 +190,11 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { if diags.HasError() { return diags } - expectedNestedWriteOnlyStr := "fakepassword" - if nestedWriteOnlyStr.AsString() != expectedNestedWriteOnlyStr { - return diag.Errorf("expected `nested_list_block.0.writeonly_string` to be: %s, got: %s", expectedNestedWriteOnlyStr, nestedWriteOnlyStr.AsString()) + if !nestedWriteOnlyStr.IsNull() { + expectedNestedWriteOnlyStr := "fakepassword" + if nestedWriteOnlyStr.AsString() != expectedNestedWriteOnlyStr { + return diag.Errorf("expected `nested_list_block.0.writeonly_string` to be: %s, got: %s", expectedNestedWriteOnlyStr, nestedWriteOnlyStr.AsString()) + } } // Double nested set block with write-only attribute @@ -212,16 +211,15 @@ func verifyWriteOnlyData(d *schema.ResourceData) diag.Diagnostics { setSlice := setBlockVal.AsValueSlice() doubleNestedWriteOnlyStr := setSlice[0].GetAttr("writeonly_string") - if diags.HasError() { - return diags - } - expecteDoubleNestedWriteOnlyStr := "fakepassword" - if doubleNestedWriteOnlyStr.AsString() != expecteDoubleNestedWriteOnlyStr { - return diag.Errorf("expected `nested_list_block.0.double_nested_set_block.0.writeonly_string` to be: %s, got: %s", expecteDoubleNestedWriteOnlyStr, doubleNestedWriteOnlyStr.AsString()) + if !doubleNestedWriteOnlyStr.IsNull() { + expecteDoubleNestedWriteOnlyStr := "fakepassword" + if doubleNestedWriteOnlyStr.AsString() != expecteDoubleNestedWriteOnlyStr { + return diag.Errorf("expected `nested_list_block.0.double_nested_set_block.0.writeonly_string` to be: %s, got: %s", expecteDoubleNestedWriteOnlyStr, doubleNestedWriteOnlyStr.AsString()) + } } // We can only set the root list, so this function also grabs data from ResourceData to ensure we use computed/default data as well - err = d.Set("nested_list_block", []map[string]any{ + err := d.Set("nested_list_block", []map[string]any{ { "string_attr": d.Get("nested_list_block.0.string_attr"), "opt_or_computed_string_attr": d.Get("nested_list_block.0.opt_or_computed_string_attr"), diff --git a/internal/sdkv2provider/resource_write_only_test.go b/internal/sdkv2provider/resource_write_only_test.go index 93a5b81..297bb8b 100644 --- a/internal/sdkv2provider/resource_write_only_test.go +++ b/internal/sdkv2provider/resource_write_only_test.go @@ -1,6 +1,7 @@ package sdkv2 import ( + "regexp" "testing" "github.com/hashicorp/go-version" @@ -209,3 +210,79 @@ func TestWriteOnlyResource(t *testing.T) { }, }) } + +func TestWriteOnlyResource_OldTerraformVersion_Error(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Run on all Terraform versions that don't support write-only attributes + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.11.0"))), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly" "test" { + string_attr = "hello!" + writeonly_string = "fakepassword" + nested_list_block { + string_attr = "hello!" + double_nested_set_block { + string_attr = "hello!" + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "corner_writeonly" "test" { + string_attr = "hello!" + nested_list_block { + string_attr = "hello!" + writeonly_string = "fakepassword" + double_nested_set_block { + string_attr = "hello!" + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "corner_writeonly" "test" { + string_attr = "hello!" + nested_list_block { + string_attr = "hello!" + double_nested_set_block { + string_attr = "hello!" + writeonly_string = "fakepassword" + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + }, + }) +} + +func TestWriteOnlyResource_NoWriteOnlyValuesSet(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Since there are no write-only values set (despite the schema defining them), this test + // should pass on all Terraform versions. + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly" "test" { + string_attr = "hello!" + nested_list_block { + string_attr = "hello!" + double_nested_set_block { + string_attr = "hello!" + } + } + }`, + }, + }, + }) +} From 4229766b809c25be65704bf06edb53846a567165 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 13:12:06 -0500 Subject: [PATCH 09/35] verify config on update --- .../framework5provider/writeonly_resource.go | 16 +++++++++++++--- .../framework6provider/writeonly_resource.go | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/internal/framework5provider/writeonly_resource.go b/internal/framework5provider/writeonly_resource.go index 1aa8a11..43bceb6 100644 --- a/internal/framework5provider/writeonly_resource.go +++ b/internal/framework5provider/writeonly_resource.go @@ -111,15 +111,25 @@ func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data WriteOnlyResourceModel + var plan WriteOnlyResourceModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } func (r WriteOnlyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go index 684ac31..50eba9c 100644 --- a/internal/framework6provider/writeonly_resource.go +++ b/internal/framework6provider/writeonly_resource.go @@ -153,15 +153,25 @@ func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data WriteOnlyResourceModel + var plan WriteOnlyResourceModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } func (r WriteOnlyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { From 8e9d941a04b949dd34bdbd22c6ab7abbe80e358f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 13:35:20 -0500 Subject: [PATCH 10/35] add nested attribute with write-only attr --- .../framework6provider/writeonly_resource.go | 58 +++++++++++++------ .../writeonly_resource_test.go | 30 ++++++++++ 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go index 50eba9c..96dcc54 100644 --- a/internal/framework6provider/writeonly_resource.go +++ b/internal/framework6provider/writeonly_resource.go @@ -46,22 +46,20 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r WriteOnly: true, ElementType: types.StringType, }, - // TODO: At the moment, this raises an invalid plan error in Terraform core (Provider is successfully planning null, core - // is rejecting this null value, instead saying it should be the config value, which is a bug) - // - https://hashicorp.slack.com/archives/C071HC4JJCC/p1734465766242319?thread_ts=1734465749.748579&cid=C071HC4JJCC - // - // "nested_object": schema.SingleNestedAttribute{ - // Required: true, - // Attributes: map[string]schema.Attribute{ - // "string_attr": schema.StringAttribute{ - // Required: true, - // }, - // "writeonly_float64": schema.Float64Attribute{ - // Required: true, - // WriteOnly: true, - // }, - // }, - // }, + "nested_map": schema.MapNestedAttribute{ + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_float64": schema.Float64Attribute{ + Required: true, + WriteOnly: true, + }, + }, + }, + }, "writeonly_nested_object": schema.SingleNestedAttribute{ Required: true, WriteOnly: true, @@ -182,6 +180,7 @@ type WriteOnlyResourceModel struct { WriteOnlyString types.String `tfsdk:"writeonly_string"` WriteOnlySet types.Set `tfsdk:"writeonly_set"` WriteOnlyNestedObject types.Object `tfsdk:"writeonly_nested_object"` + NestedMap types.Map `tfsdk:"nested_map"` NestedBlockList types.List `tfsdk:"nested_block_list"` } @@ -216,7 +215,32 @@ func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { ) } - // Nested write-only attribute + // Nested map with write-only attribute + expectedMapObjType := types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string_attr": types.StringType, + "writeonly_float64": types.Float64Type, + }, + } + expectedMap := types.MapValueMust(expectedMapObjType, map[string]attr.Value{ + "key1": types.ObjectValueMust(expectedMapObjType.AttributeTypes(), map[string]attr.Value{ + "string_attr": types.StringValue("hello"), + "writeonly_float64": types.Float64Value(10), + }), + "key2": types.ObjectValueMust(expectedMapObjType.AttributeTypes(), map[string]attr.Value{ + "string_attr": types.StringValue("world"), + "writeonly_float64": types.Float64Value(20), + }), + }) + if !m.NestedMap.Equal(expectedMap) { + return diag.NewAttributeErrorDiagnostic( + path.Root("nested_map"), + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedMap, m.NestedMap), + ) + } + + // Nested write-only object attribute expectedListObjType := types.ObjectType{ AttrTypes: map[string]attr.Type{ "writeonly_string": types.StringType, diff --git a/internal/framework6provider/writeonly_resource_test.go b/internal/framework6provider/writeonly_resource_test.go index 0e54976..d2e4590 100644 --- a/internal/framework6provider/writeonly_resource_test.go +++ b/internal/framework6provider/writeonly_resource_test.go @@ -47,6 +47,16 @@ func TestWriteOnlyResource(t *testing.T) { } ] } + nested_map = { + "key1": { + string_attr = "hello" + writeonly_float64 = 10 + } + "key2": { + string_attr = "world" + writeonly_float64 = 20 + } + } nested_block_list { string_attr = "hello" writeonly_string = "fakepassword1" @@ -72,6 +82,16 @@ func TestWriteOnlyResource(t *testing.T) { plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_map"), knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_float64": knownvalue.Null(), + }), + "key2": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_float64": knownvalue.Null(), + }), + })), plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ knownvalue.ObjectExact(map[string]knownvalue.Check{ "string_attr": knownvalue.StringExact("hello"), @@ -97,6 +117,16 @@ func TestWriteOnlyResource(t *testing.T) { statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_map"), knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_float64": knownvalue.Null(), + }), + "key2": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_float64": knownvalue.Null(), + }), + })), statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ knownvalue.ObjectExact(map[string]knownvalue.Check{ "string_attr": knownvalue.StringExact("hello"), From b73b0342f1b9fc808da4d8ab44286b2966b1adb8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 13:43:01 -0500 Subject: [PATCH 11/35] remove unused schema attribute --- internal/protocolprovider/server.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/protocolprovider/server.go b/internal/protocolprovider/server.go index 6fa8bca..f50bf54 100644 --- a/internal/protocolprovider/server.go +++ b/internal/protocolprovider/server.go @@ -95,10 +95,7 @@ func (s *server) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRe func Server() tfprotov5.ProviderServer { return &server{ providerSchema: &tfprotov5.Schema{ - Block: &tfprotov5.SchemaBlock{ - Attributes: []*tfprotov5.SchemaAttribute{ - {Name: "deferral", Type: tftypes.Bool, Optional: true}}, - }, + Block: &tfprotov5.SchemaBlock{}, }, dataSourceSchemas: map[string]*tfprotov5.Schema{ "corner_time": { From e08f258c7809e13a14413e7d5e979f27fff9d030 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 13:45:22 -0500 Subject: [PATCH 12/35] remove unused provider attribute --- internal/sdkv2testingprovider/provider.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/sdkv2testingprovider/provider.go b/internal/sdkv2testingprovider/provider.go index 0cb7a6f..85f46e3 100644 --- a/internal/sdkv2testingprovider/provider.go +++ b/internal/sdkv2testingprovider/provider.go @@ -14,12 +14,6 @@ import ( func New() *schema.Provider { p := &schema.Provider{ - Schema: map[string]*schema.Schema{ - "deferral": { - Type: schema.TypeBool, - Optional: true, - }, - }, DataSourcesMap: map[string]*schema.Resource{ "corner_regions": dataSourceRegions(), "corner_bigint": dataSourceBigint(), From b9c211d18fd5fc1278491b3de16c08a39a5dd25a Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 13:45:40 -0500 Subject: [PATCH 13/35] diff --- internal/sdkv2testingprovider/provider.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/sdkv2testingprovider/provider.go b/internal/sdkv2testingprovider/provider.go index 85f46e3..f2b28b6 100644 --- a/internal/sdkv2testingprovider/provider.go +++ b/internal/sdkv2testingprovider/provider.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-provider-corner/internal/backend" ) From ffa3961dc9852e78806ff4640fa2aaf444172abd Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 14:24:42 -0500 Subject: [PATCH 14/35] refactor test to avoid paths --- .../sdkv2provider/resource_write_only_test.go | 178 ++++++------------ 1 file changed, 57 insertions(+), 121 deletions(-) diff --git a/internal/sdkv2provider/resource_write_only_test.go b/internal/sdkv2provider/resource_write_only_test.go index 297bb8b..7aa5b05 100644 --- a/internal/sdkv2provider/resource_write_only_test.go +++ b/internal/sdkv2provider/resource_write_only_test.go @@ -46,36 +46,20 @@ func TestWriteOnlyResource(t *testing.T) { plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("hello!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("computed value!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("hello!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("computed value!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("nested_list_block"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello!"), + "opt_or_computed_string_attr": knownvalue.StringExact("computed value!"), + "writeonly_string": knownvalue.Null(), + "double_nested_set_block": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello!"), + "opt_or_computed_string_attr": knownvalue.StringExact("computed value!"), + "writeonly_string": knownvalue.Null(), + }), + }), + }), + })), }, }, ConfigStateChecks: []statecheck.StateCheck{ @@ -83,36 +67,20 @@ func TestWriteOnlyResource(t *testing.T) { statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("hello!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("computed value!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("hello!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("computed value!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("nested_list_block"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello!"), + "opt_or_computed_string_attr": knownvalue.StringExact("computed value!"), + "writeonly_string": knownvalue.Null(), + "double_nested_set_block": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello!"), + "opt_or_computed_string_attr": knownvalue.StringExact("computed value!"), + "writeonly_string": knownvalue.Null(), + }), + }), + }), + })), }, }, { @@ -138,36 +106,20 @@ func TestWriteOnlyResource(t *testing.T) { plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("world!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("config value!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("world!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("config value!"), - ), - plancheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), + plancheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("nested_list_block"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world!"), + "opt_or_computed_string_attr": knownvalue.StringExact("config value!"), + "writeonly_string": knownvalue.Null(), + "double_nested_set_block": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world!"), + "opt_or_computed_string_attr": knownvalue.StringExact("config value!"), + "writeonly_string": knownvalue.Null(), + }), + }), + }), + })), }, }, ConfigStateChecks: []statecheck.StateCheck{ @@ -175,36 +127,20 @@ func TestWriteOnlyResource(t *testing.T) { statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_bool"), knownvalue.Null()), statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("writeonly_int"), knownvalue.Null()), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("world!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("config value!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("string_attr"), - knownvalue.StringExact("world!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("opt_or_computed_string_attr"), - knownvalue.StringExact("config value!"), - ), - statecheck.ExpectKnownValue( - "corner_writeonly.test", - tfjsonpath.New("nested_list_block").AtSliceIndex(0).AtMapKey("double_nested_set_block").AtSliceIndex(0).AtMapKey("writeonly_string"), - knownvalue.Null(), - ), + statecheck.ExpectKnownValue("corner_writeonly.test", tfjsonpath.New("nested_list_block"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world!"), + "opt_or_computed_string_attr": knownvalue.StringExact("config value!"), + "writeonly_string": knownvalue.Null(), + "double_nested_set_block": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world!"), + "opt_or_computed_string_attr": knownvalue.StringExact("config value!"), + "writeonly_string": knownvalue.Null(), + }), + }), + }), + })), }, }, }, @@ -217,7 +153,7 @@ func TestWriteOnlyResource_OldTerraformVersion_Error(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Run on all Terraform versions that don't support write-only attributes TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipAbove(version.Must(version.NewVersion("1.11.0"))), + tfversion.SkipAbove(tfversion.Version1_10_0), }, Providers: testAccProviders, Steps: []resource.TestStep{ From 200d66f69838a57128b19056a2dde7572ed20fdd Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 16:25:52 -0500 Subject: [PATCH 15/35] add negative tests for framework providers w/ old terraform versions --- go.mod | 2 +- go.sum | 4 +- .../framework5provider/writeonly_resource.go | 117 +++----- .../writeonly_resource_test.go | 121 ++++++++- .../framework6provider/writeonly_resource.go | 154 +++-------- .../writeonly_resource_test.go | 254 +++++++++++++++++- 6 files changed, 455 insertions(+), 197 deletions(-) diff --git a/go.mod b/go.mod index 5145f88..6f6cd86 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-memdb v1.3.4 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-json v0.24.0 - github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08 + github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20250102211725-428efefe5acc github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 diff --git a/go.sum b/go.sum index 9c794b3..117c149 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVW github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= -github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08 h1:JS11x8PqVqNbFMa0g6BNYcBxJcWy9lc2EI0ksknL+Tw= -github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20241217183656-f4f97bdd3a08/go.mod h1:qBKUqe1lv1NZcsO5pBjiKd5YuNDUeqvV1w8w5df/8WI= +github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20250102211725-428efefe5acc h1:05/WFc86cMPmw85GuVdoPRpBq9iQ7YyR9pYIsRcI0zs= +github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20250102211725-428efefe5acc/go.mod h1:qBKUqe1lv1NZcsO5pBjiKd5YuNDUeqvV1w8w5df/8WI= github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 h1:Zap24rkky7SvNGGNYHMKFhAriP6+6riI21BMYOYgLRE= github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0/go.mod h1:CYPq+I5bWsmI8021VJY85hAyOeiEEQpdGW+NapdQn7A= github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E= diff --git a/internal/framework5provider/writeonly_resource.go b/internal/framework5provider/writeonly_resource.go index 43bceb6..940c627 100644 --- a/internal/framework5provider/writeonly_resource.go +++ b/internal/framework5provider/writeonly_resource.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -33,16 +34,16 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "writeonly_custom_ipv6": schema.StringAttribute{ - Required: true, + Optional: true, WriteOnly: true, CustomType: iptypes.IPv6AddressType{}, }, "writeonly_string": schema.StringAttribute{ - Required: true, + Optional: true, WriteOnly: true, }, "writeonly_set": schema.SetAttribute{ - Required: true, + Optional: true, WriteOnly: true, ElementType: types.StringType, }, @@ -55,7 +56,7 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r Required: true, }, "writeonly_string": schema.StringAttribute{ - Required: true, + Optional: true, WriteOnly: true, }, }, @@ -66,7 +67,7 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r Required: true, }, "writeonly_bool": schema.BoolAttribute{ - Required: true, + Optional: true, WriteOnly: true, }, }, @@ -84,13 +85,8 @@ func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateReques if resp.Diagnostics.HasError() { return } - var config WriteOnlyResourceModel - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if resp.Diagnostics.HasError() { - return - } - resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + resp.Diagnostics.Append(VerifyWriteOnlyData(ctx, req.Config)...) if resp.Diagnostics.HasError() { return } @@ -118,13 +114,7 @@ func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateReques return } - var config WriteOnlyResourceModel - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + resp.Diagnostics.Append(VerifyWriteOnlyData(ctx, req.Config)...) if resp.Diagnostics.HasError() { return } @@ -144,74 +134,41 @@ type WriteOnlyResourceModel struct { // VerifyWriteOnlyData compares the hardcoded test data for the write-only attributes in this resource, raising // error diagnostics if the data differs from expectations. -func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { +func VerifyWriteOnlyData(ctx context.Context, cfg tfsdk.Config) diag.Diagnostics { + var diags diag.Diagnostics // Primitive write-only attributes - expectedCustomIPv6 := "::" - expectedString := "fakepassword" - if m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { - return diag.NewAttributeErrorDiagnostic( - path.Root("writeonly_custom_ipv6"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6), - ) - } - if m.WriteOnlyString.ValueString() != expectedString { - return diag.NewAttributeErrorDiagnostic( - path.Root("writeonly_string"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString), - ) - } + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("writeonly_custom_ipv6"), iptypes.NewIPv6AddressValue("::"))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("writeonly_string"), types.StringValue("fakepassword"))...) // Collection write-only attribute - expectedSet := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}) - if !m.WriteOnlySet.Equal(expectedSet) { - return diag.NewAttributeErrorDiagnostic( - path.Root("writeonly_set"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %s, got: %s", expectedSet, m.WriteOnlySet), - ) - } + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("writeonly_set"), types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}))...) // Nested block with write-only attributes - expectedBlockObjectType := types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "bool_attr": types.BoolType, - "writeonly_bool": types.BoolType, - }, - } - expectedBlockListObjType := types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string_attr": types.StringType, - "writeonly_string": types.StringType, - "double_nested_object": expectedBlockObjectType, - }, + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(0).AtName("writeonly_string"), types.StringValue("fakepassword1"))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(1).AtName("writeonly_string"), types.StringValue("fakepassword2"))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(0).AtName("double_nested_object").AtName("writeonly_bool"), types.BoolValue(false))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(1).AtName("double_nested_object").AtName("writeonly_bool"), types.BoolValue(true))...) + + return diags +} + +// Asserts a write-only value in configuration, if the value is null it will return without an error (allowing the attribute to be optional) +func assertWriteOnlyVal[T attr.Value](ctx context.Context, cfg tfsdk.Config, p path.Path, expectedVal T) diag.Diagnostics { + var writeOnlyVal T + diags := cfg.GetAttribute(ctx, p, &writeOnlyVal) + if diags.HasError() { + // All the paths are hardcoded in the resource, so this scenario shouldn't occur unless there is a schema/path mismatch or addition + return diags } - expectedListBlock := types.ListValueMust(expectedBlockListObjType, []attr.Value{ - types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ - "string_attr": types.StringValue("hello"), - "writeonly_string": types.StringValue("fakepassword1"), - "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ - "bool_attr": types.BoolValue(true), - "writeonly_bool": types.BoolValue(false), - }), - }), - types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ - "string_attr": types.StringValue("world"), - "writeonly_string": types.StringValue("fakepassword2"), - "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ - "bool_attr": types.BoolValue(false), - "writeonly_bool": types.BoolValue(true), - }), - }), - }) - - if !m.NestedBlockList.Equal(expectedListBlock) { - return diag.NewAttributeErrorDiagnostic( - path.Root("nested_block_list"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %s, got: %s", expectedListBlock, m.NestedBlockList), - ) + + if !writeOnlyVal.IsNull() && !writeOnlyVal.Equal(expectedVal) { + return diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + p, + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedVal, writeOnlyVal), + ), + } } return nil diff --git a/internal/framework5provider/writeonly_resource_test.go b/internal/framework5provider/writeonly_resource_test.go index d46f0f9..ac9a95a 100644 --- a/internal/framework5provider/writeonly_resource_test.go +++ b/internal/framework5provider/writeonly_resource_test.go @@ -4,6 +4,7 @@ package framework import ( + "regexp" "testing" "github.com/hashicorp/go-version" @@ -106,4 +107,122 @@ func TestWriteOnlyResource(t *testing.T) { }) } -// TODO: add negative tests for old terraform versions +func TestWriteOnlyResource_OldTerraformVersion_Error(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Run on all Terraform versions that don't support write-only attributes + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_custom_ipv6 = "::" + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + writeonly_set = ["fake", "password"] + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + nested_block_list { + string_attr = "hello" + writeonly_string = "fakepassword1" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + writeonly_string = "fakepassword2" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + writeonly_bool = false + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + writeonly_bool = true + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + }, + }) +} + +func TestWriteOnlyResource_NoWriteOnlyValuesSet(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Since there are no write-only values set (despite the schema defining them), this test + // should pass on all Terraform versions. + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + }, + }, + }) +} diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go index 96dcc54..f4fd965 100644 --- a/internal/framework6provider/writeonly_resource.go +++ b/internal/framework6provider/writeonly_resource.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -33,16 +34,16 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "writeonly_custom_ipv6": schema.StringAttribute{ - Required: true, + Optional: true, WriteOnly: true, CustomType: iptypes.IPv6AddressType{}, }, "writeonly_string": schema.StringAttribute{ - Required: true, + Optional: true, WriteOnly: true, }, "writeonly_set": schema.SetAttribute{ - Required: true, + Optional: true, WriteOnly: true, ElementType: types.StringType, }, @@ -54,14 +55,14 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r Required: true, }, "writeonly_float64": schema.Float64Attribute{ - Required: true, + Optional: true, WriteOnly: true, }, }, }, }, "writeonly_nested_object": schema.SingleNestedAttribute{ - Required: true, + Optional: true, WriteOnly: true, Attributes: map[string]schema.Attribute{ "writeonly_int64": schema.Int64Attribute{ @@ -95,7 +96,7 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r Required: true, }, "writeonly_string": schema.StringAttribute{ - Required: true, + Optional: true, WriteOnly: true, }, }, @@ -106,7 +107,7 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r Required: true, }, "writeonly_bool": schema.BoolAttribute{ - Required: true, + Optional: true, WriteOnly: true, }, }, @@ -124,13 +125,8 @@ func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateReques if resp.Diagnostics.HasError() { return } - var config WriteOnlyResourceModel - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if resp.Diagnostics.HasError() { - return - } - resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + resp.Diagnostics.Append(VerifyWriteOnlyData(ctx, req.Config)...) if resp.Diagnostics.HasError() { return } @@ -158,13 +154,7 @@ func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateReques return } - var config WriteOnlyResourceModel - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(config.VerifyWriteOnlyData()) + resp.Diagnostics.Append(VerifyWriteOnlyData(ctx, req.Config)...) if resp.Diagnostics.HasError() { return } @@ -186,59 +176,18 @@ type WriteOnlyResourceModel struct { // VerifyWriteOnlyData compares the hardcoded test data for the write-only attributes in this resource, raising // error diagnostics if the data differs from expectations. -func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { +func VerifyWriteOnlyData(ctx context.Context, cfg tfsdk.Config) diag.Diagnostics { + var diags diag.Diagnostics // Primitive write-only attributes - expectedCustomIPv6 := "::" - expectedString := "fakepassword" - if m.WriteOnlyCustomIPv6.ValueString() != expectedCustomIPv6 { - return diag.NewAttributeErrorDiagnostic( - path.Root("writeonly_custom_ipv6"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedCustomIPv6, m.WriteOnlyCustomIPv6), - ) - } - if m.WriteOnlyString.ValueString() != expectedString { - return diag.NewAttributeErrorDiagnostic( - path.Root("writeonly_string"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %q, got: %q", expectedString, m.WriteOnlyString), - ) - } + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("writeonly_custom_ipv6"), iptypes.NewIPv6AddressValue("::"))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("writeonly_string"), types.StringValue("fakepassword"))...) // Collection write-only attribute - expectedSet := types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}) - if !m.WriteOnlySet.Equal(expectedSet) { - return diag.NewAttributeErrorDiagnostic( - path.Root("writeonly_set"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %s, got: %s", expectedSet, m.WriteOnlySet), - ) - } + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("writeonly_set"), types.SetValueMust(types.StringType, []attr.Value{types.StringValue("fake"), types.StringValue("password")}))...) // Nested map with write-only attribute - expectedMapObjType := types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string_attr": types.StringType, - "writeonly_float64": types.Float64Type, - }, - } - expectedMap := types.MapValueMust(expectedMapObjType, map[string]attr.Value{ - "key1": types.ObjectValueMust(expectedMapObjType.AttributeTypes(), map[string]attr.Value{ - "string_attr": types.StringValue("hello"), - "writeonly_float64": types.Float64Value(10), - }), - "key2": types.ObjectValueMust(expectedMapObjType.AttributeTypes(), map[string]attr.Value{ - "string_attr": types.StringValue("world"), - "writeonly_float64": types.Float64Value(20), - }), - }) - if !m.NestedMap.Equal(expectedMap) { - return diag.NewAttributeErrorDiagnostic( - path.Root("nested_map"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %s, got: %s", expectedMap, m.NestedMap), - ) - } + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_map").AtMapKey("key1").AtName("writeonly_float64"), types.Float64Value(10))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_map").AtMapKey("key2").AtName("writeonly_float64"), types.Float64Value(20))...) // Nested write-only object attribute expectedListObjType := types.ObjectType{ @@ -264,53 +213,34 @@ func (m WriteOnlyResourceModel) VerifyWriteOnlyData() diag.Diagnostic { }), }) - if !m.WriteOnlyNestedObject.Equal(expectedObject) { - return diag.NewAttributeErrorDiagnostic( - path.Root("writeonly_nested_object"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %s, got: %s", expectedObject, m.WriteOnlyNestedObject), - ) - } + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("writeonly_nested_object"), expectedObject)...) // Nested block with write-only attributes - expectedBlockObjectType := types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "bool_attr": types.BoolType, - "writeonly_bool": types.BoolType, - }, - } - expectedBlockListObjType := types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "string_attr": types.StringType, - "writeonly_string": types.StringType, - "double_nested_object": expectedBlockObjectType, - }, - } - expectedListBlock := types.ListValueMust(expectedBlockListObjType, []attr.Value{ - types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ - "string_attr": types.StringValue("hello"), - "writeonly_string": types.StringValue("fakepassword1"), - "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ - "bool_attr": types.BoolValue(true), - "writeonly_bool": types.BoolValue(false), - }), - }), - types.ObjectValueMust(expectedBlockListObjType.AttributeTypes(), map[string]attr.Value{ - "string_attr": types.StringValue("world"), - "writeonly_string": types.StringValue("fakepassword2"), - "double_nested_object": types.ObjectValueMust(expectedBlockObjectType.AttributeTypes(), map[string]attr.Value{ - "bool_attr": types.BoolValue(false), - "writeonly_bool": types.BoolValue(true), - }), - }), - }) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(0).AtName("writeonly_string"), types.StringValue("fakepassword1"))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(1).AtName("writeonly_string"), types.StringValue("fakepassword2"))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(0).AtName("double_nested_object").AtName("writeonly_bool"), types.BoolValue(false))...) + diags.Append(assertWriteOnlyVal(ctx, cfg, path.Root("nested_block_list").AtListIndex(1).AtName("double_nested_object").AtName("writeonly_bool"), types.BoolValue(true))...) + + return diags +} - if !m.NestedBlockList.Equal(expectedListBlock) { - return diag.NewAttributeErrorDiagnostic( - path.Root("nested_block_list"), - "Unexpected WriteOnly Value", - fmt.Sprintf("wanted: %s, got: %s", expectedListBlock, m.NestedBlockList), - ) +// Asserts a write-only value in configuration, if the value is null it will return without an error (allowing the attribute to be optional) +func assertWriteOnlyVal[T attr.Value](ctx context.Context, cfg tfsdk.Config, p path.Path, expectedVal T) diag.Diagnostics { + var writeOnlyVal T + diags := cfg.GetAttribute(ctx, p, &writeOnlyVal) + if diags.HasError() { + // All the paths are hardcoded in the resource, so this scenario shouldn't occur unless there is a schema/path mismatch or addition + return diags + } + + if !writeOnlyVal.IsNull() && !writeOnlyVal.Equal(expectedVal) { + return diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + p, + "Unexpected WriteOnly Value", + fmt.Sprintf("wanted: %s, got: %s", expectedVal, writeOnlyVal), + ), + } } return nil diff --git a/internal/framework6provider/writeonly_resource_test.go b/internal/framework6provider/writeonly_resource_test.go index d2e4590..0023c2f 100644 --- a/internal/framework6provider/writeonly_resource_test.go +++ b/internal/framework6provider/writeonly_resource_test.go @@ -4,6 +4,7 @@ package framework import ( + "regexp" "testing" "github.com/hashicorp/go-version" @@ -151,4 +152,255 @@ func TestWriteOnlyResource(t *testing.T) { }) } -// TODO: add negative tests for old terraform versions +func TestWriteOnlyResource_OldTerraformVersion_Error(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Run on all Terraform versions that don't support write-only attributes + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + writeonly_custom_ipv6 = "::" + nested_map = { + "key1": { + string_attr = "hello" + } + "key2": { + string_attr = "world" + } + } + nested_block_list { + string_attr = "hello" + + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + writeonly_string = "fakepassword" + nested_map = { + "key1": { + string_attr = "hello" + } + "key2": { + string_attr = "world" + } + } + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + writeonly_set = ["fake", "password"] + nested_map = { + "key1": { + string_attr = "hello" + } + "key2": { + string_attr = "world" + } + } + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + writeonly_nested_object = { + writeonly_int64 = 1234 + writeonly_bool = true + + writeonly_nested_list = [ + { + writeonly_string = "fakepassword1" + }, + { + writeonly_string = "fakepassword2" + } + ] + } + nested_map = { + "key1": { + string_attr = "hello" + } + "key2": { + string_attr = "world" + } + } + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + nested_map = { + "key1": { + string_attr = "hello" + writeonly_float64 = 10 + } + "key2": { + string_attr = "world" + writeonly_float64 = 20 + } + } + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + nested_map = { + "key1": { + string_attr = "hello" + } + "key2": { + string_attr = "world" + } + } + nested_block_list { + string_attr = "hello" + writeonly_string = "fakepassword1" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + writeonly_string = "fakepassword2" + double_nested_object { + bool_attr = false + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + { + Config: `resource "framework_writeonly" "test" { + nested_map = { + "key1": { + string_attr = "hello" + } + "key2": { + string_attr = "world" + } + } + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + writeonly_bool = false + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + writeonly_bool = true + } + } + }`, + ExpectError: regexp.MustCompile(`WriteOnly Attribute Not Allowed`), + }, + }, + }) +} + +func TestWriteOnlyResource_NoWriteOnlyValuesSet(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Since there are no write-only values set (despite the schema defining them), this test + // should pass on all Terraform versions. + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly" "test" { + nested_map = { + "key1": { + string_attr = "hello" + } + "key2": { + string_attr = "world" + } + } + nested_block_list { + string_attr = "hello" + double_nested_object { + bool_attr = true + } + } + nested_block_list { + string_attr = "world" + double_nested_object { + bool_attr = false + } + } + }`, + }, + }, + }) +} From a75e3daf82bd8fbbcb69240383f0c82125470c51 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 2 Jan 2025 16:33:58 -0500 Subject: [PATCH 16/35] testing version from pkg --- go.mod | 4 ++-- go.sum | 4 ++-- internal/framework5provider/writeonly_resource_test.go | 3 +-- internal/framework6provider/writeonly_resource_test.go | 3 +-- internal/sdkv2provider/resource_write_only_test.go | 3 +-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 6f6cd86..432f9a1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.22.7 require ( github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-memdb v1.3.4 - github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-json v0.24.0 github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20250102211725-428efefe5acc github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 @@ -14,7 +13,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1 - github.com/hashicorp/terraform-plugin-testing v1.11.0 + github.com/hashicorp/terraform-plugin-testing v1.11.1-0.20250102212914-99297ce85154 github.com/zclconf/go-cty v1.15.1 ) @@ -35,6 +34,7 @@ require ( github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hc-install v0.9.0 // indirect github.com/hashicorp/hcl/v2 v2.23.0 // indirect diff --git a/go.sum b/go.sum index 117c149..bb30340 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,8 @@ github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de/go.mod h1:To/3Wdozxp5QLvgcR4lKOdfiBcG70eoFZepcXro+nPQ= github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1 h1:pUl5Z8PQWNDz/VGIl8RGaSATG+Ja7W31+u6jJYRtiPw= github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1/go.mod h1:wsPZ9W0E/8tID4aejTrYrNPlJS8YhkEepeMeZlUaB8M= -github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= -github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U= +github.com/hashicorp/terraform-plugin-testing v1.11.1-0.20250102212914-99297ce85154 h1:uK2Y1p/2RqvCRxytmByMywbKNdTpxvarGDDoiA7Qkcg= +github.com/hashicorp/terraform-plugin-testing v1.11.1-0.20250102212914-99297ce85154/go.mod h1:/HZY+vE/VIsnSNEkvgdpIiuMp6gT+u6HwTf6vs5Cfkk= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/internal/framework5provider/writeonly_resource_test.go b/internal/framework5provider/writeonly_resource_test.go index ac9a95a..778bff0 100644 --- a/internal/framework5provider/writeonly_resource_test.go +++ b/internal/framework5provider/writeonly_resource_test.go @@ -7,7 +7,6 @@ import ( "regexp" "testing" - "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -24,7 +23,7 @@ func TestWriteOnlyResource(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Write-only attributes are only available in 1.11.0+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + tfversion.SkipBelow(tfversion.Version1_11_0), }, ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ "framework": providerserver.NewProtocol5WithError(New()), diff --git a/internal/framework6provider/writeonly_resource_test.go b/internal/framework6provider/writeonly_resource_test.go index 0023c2f..3d539c7 100644 --- a/internal/framework6provider/writeonly_resource_test.go +++ b/internal/framework6provider/writeonly_resource_test.go @@ -7,7 +7,6 @@ import ( "regexp" "testing" - "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -24,7 +23,7 @@ func TestWriteOnlyResource(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Write-only attributes are only available in 1.11.0+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + tfversion.SkipBelow(tfversion.Version1_11_0), }, ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "framework": providerserver.NewProtocol6WithError(New()), diff --git a/internal/sdkv2provider/resource_write_only_test.go b/internal/sdkv2provider/resource_write_only_test.go index 7aa5b05..2d86cff 100644 --- a/internal/sdkv2provider/resource_write_only_test.go +++ b/internal/sdkv2provider/resource_write_only_test.go @@ -4,7 +4,6 @@ import ( "regexp" "testing" - "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/plancheck" @@ -21,7 +20,7 @@ func TestWriteOnlyResource(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Write-only attributes are only available in 1.11 and later TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(version.Must(version.NewVersion("1.11.0"))), + tfversion.SkipBelow(tfversion.Version1_11_0), }, Providers: testAccProviders, Steps: []resource.TestStep{ From 05cdb67d88036d157517b452ed1fa8d4eeb38538 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 3 Jan 2025 15:29:58 -0500 Subject: [PATCH 17/35] write-once resource --- go.mod | 2 +- go.sum | 4 +- internal/sdkv2provider/provider.go | 1 + internal/sdkv2provider/resource_write_once.go | 103 ++++++++++++ .../sdkv2provider/resource_write_once_test.go | 148 ++++++++++++++++++ 5 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 internal/sdkv2provider/resource_write_once.go create mode 100644 internal/sdkv2provider/resource_write_once_test.go diff --git a/go.mod b/go.mod index 432f9a1..b81190d 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de - github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20250103201413-5f29273ad6f0 github.com/hashicorp/terraform-plugin-testing v1.11.1-0.20250102212914-99297ce85154 github.com/zclconf/go-cty v1.15.1 ) diff --git a/go.sum b/go.sum index bb30340..0c3ac8c 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9T github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de h1:qjSTbKECIWAEO5hBGqabykC/xwrAYIrD7fKSVKldFhY= github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de/go.mod h1:To/3Wdozxp5QLvgcR4lKOdfiBcG70eoFZepcXro+nPQ= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1 h1:pUl5Z8PQWNDz/VGIl8RGaSATG+Ja7W31+u6jJYRtiPw= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20241219185136-ec6675f709c1/go.mod h1:wsPZ9W0E/8tID4aejTrYrNPlJS8YhkEepeMeZlUaB8M= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20250103201413-5f29273ad6f0 h1:wBngDRqcqKObWtg/2e/FPcMaL9SLtkBePd5adYjCcMo= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20250103201413-5f29273ad6f0/go.mod h1:wsPZ9W0E/8tID4aejTrYrNPlJS8YhkEepeMeZlUaB8M= github.com/hashicorp/terraform-plugin-testing v1.11.1-0.20250102212914-99297ce85154 h1:uK2Y1p/2RqvCRxytmByMywbKNdTpxvarGDDoiA7Qkcg= github.com/hashicorp/terraform-plugin-testing v1.11.1-0.20250102212914-99297ce85154/go.mod h1:/HZY+vE/VIsnSNEkvgdpIiuMp6gT+u6HwTf6vs5Cfkk= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index 0eae168..f2e4e1b 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -28,6 +28,7 @@ func New() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "corner_user": resourceUser(), "corner_writeonly": resourceWriteOnly(), + "corner_writeonce": resourceWriteOnce(), "corner_bigint": resourceBigint(), "corner_user_cty": resourceUserCty(), "corner_deferred_action": resourceDeferredAction(), diff --git a/internal/sdkv2provider/resource_write_once.go b/internal/sdkv2provider/resource_write_once.go new file mode 100644 index 0000000..eb4f822 --- /dev/null +++ b/internal/sdkv2provider/resource_write_once.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//nolint:forcetypeassert // Test SDK provider +package sdkv2 + +import ( + "context" + "errors" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceWriteOnce() *schema.Resource { + return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + + CreateContext: resourceWriteOnceCreate, + ReadContext: resourceWriteOnceRead, + UpdateContext: resourceWriteOnceUpdate, + DeleteContext: resourceWriteOnceDelete, + + Schema: map[string]*schema.Schema{ + "trigger_attr": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + // The only place that validation can reference prior state (which is required to determine if the planned action is + // a create, i.e, prior state is null) is during plan modification. So the customize diff implementation is responsible + // for applying the "write-once" validation + "writeonce_string": { + Type: schema.TypeString, + Optional: true, + WriteOnly: true, + }, + }, + CustomizeDiff: func(ctx context.Context, rd *schema.ResourceDiff, _ interface{}) error { + valPath := cty.GetAttrPath("writeonce_string") + + // If there is a non-null state, we are destroying or updating so no validation is needed + if !rd.GetRawState().IsNull() { + return nil + } + + configVal, diags := rd.GetRawConfigAt(valPath) // New method duplicated from (*schema.ResourceData).GetRawConfigAt + if diags.HasError() { + // This error shouldn't occur unless there is a schema change + return errors.New("error retrieving config value for write-once attribute") + } + + // We are creating, but the write-once attribute value is not present in config, return an error. + if configVal.IsNull() { + return errors.New(`"writeonce_string" is required when creating the resource, but no definition was found.`) + } + + return nil + }, + } +} + +func resourceWriteOnceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId("fakeid-123") + + // Write-only string is required on create, verify the data + strVal, diags := d.GetRawConfigAt(cty.GetAttrPath("writeonce_string")) + if diags.HasError() { + return diags + } + + expectedString := "fakepassword" + if strVal.AsString() != expectedString { + return diag.Errorf("expected `writeonce_string` to be: %q, got: %q", expectedString, strVal.AsString()) + } + + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err := d.Set("writeonce_string", "different value") + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceWriteOnceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Config isn't set for Read, so can't verify write-only data + return nil +} + +func resourceWriteOnceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Once created, the only operation that can occur is replacement (delete/create) + return nil +} + +func resourceWriteOnceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Config isn't set for Delete, so can't verify write-only data + return nil +} diff --git a/internal/sdkv2provider/resource_write_once_test.go b/internal/sdkv2provider/resource_write_once_test.go new file mode 100644 index 0000000..1cc9d96 --- /dev/null +++ b/internal/sdkv2provider/resource_write_once_test.go @@ -0,0 +1,148 @@ +package sdkv2 + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +func TestWriteOnceResource(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + { + // Now that the resource is created, we can remove the attribute with no planned changes + Config: `resource "corner_writeonce" "test" { + trigger_attr = "1" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonce.test", plancheck.ResourceActionNoop), + }, + }, + }, + { + // Write-only attributes cannot participate and plan as they will always be null in prior/proposed new state + Config: `resource "corner_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "this value cannot prompt a change on it's own" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonce.test", plancheck.ResourceActionNoop), + }, + }, + }, + { + // trigger_attr will prompt the replace action here + Config: `resource "corner_writeonce" "test" { + trigger_attr = "2" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonce.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + }, + }) +} + +func TestWriteOnceResource_error_on_create(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // This error message should occur on all Terraform versions. + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonce" "test" { + trigger_attr = "1" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("corner_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ExpectError: regexp.MustCompile(`"writeonce_string" is required when creating the resource, but no definition was found.`), + }, + }, + }) +} + +func TestWriteOnceResource_error_on_replace(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + { + Config: `resource "corner_writeonce" "test" { + trigger_attr = "2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonce.test", plancheck.ResourceActionReplace), + }, + }, + ExpectError: regexp.MustCompile(`"writeonce_string" is required when creating the resource, but no definition was found.`), + }, + }, + }) +} From 6f7403dc788fb2fe8186132bb3c63f18ae742bea Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 3 Jan 2025 17:27:27 -0500 Subject: [PATCH 18/35] more SDKv2 tests with validations --- internal/sdkv2provider/provider.go | 1 + .../resource_write_only_validations.go | 102 +++++++++++++ .../resource_write_only_validations_test.go | 140 ++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 internal/sdkv2provider/resource_write_only_validations.go create mode 100644 internal/sdkv2provider/resource_write_only_validations_test.go diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index f2e4e1b..6c622a9 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -29,6 +29,7 @@ func New() *schema.Provider { "corner_user": resourceUser(), "corner_writeonly": resourceWriteOnly(), "corner_writeonce": resourceWriteOnce(), + "corner_writeonly_validations": resourceWriteOnlyValidations(), "corner_bigint": resourceBigint(), "corner_user_cty": resourceUserCty(), "corner_deferred_action": resourceDeferredAction(), diff --git a/internal/sdkv2provider/resource_write_only_validations.go b/internal/sdkv2provider/resource_write_only_validations.go new file mode 100644 index 0000000..7357831 --- /dev/null +++ b/internal/sdkv2provider/resource_write_only_validations.go @@ -0,0 +1,102 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//nolint:forcetypeassert // Test SDK provider +package sdkv2 + +import ( + "context" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceWriteOnlyValidations() *schema.Resource { + return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + + CreateContext: resourceWriteOnlyValidationsCreate, + ReadContext: resourceWriteOnlyValidationsRead, + UpdateContext: resourceWriteOnlyValidationsUpdate, + DeleteContext: resourceWriteOnlyValidationsDelete, + + // TODO: The testing framework can't verify warning diagnostics currently + // https://github.com/hashicorp/terraform-plugin-testing/issues/69 + ValidateRawResourceConfigFuncs: []schema.ValidateRawResourceConfigFunc{ + validation.PreferWriteOnlyAttribute( + cty.GetAttrPath("old_password_attr"), + cty.GetAttrPath("writeonly_password"), + ), + }, + + Schema: map[string]*schema.Schema{ + "old_password_attr": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{"writeonly_password"}, + ConflictsWith: []string{"password_version"}, + }, + "password_version": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + RequiredWith: []string{"writeonly_password"}, + }, + "writeonly_password": { + Type: schema.TypeString, + Optional: true, + WriteOnly: true, + RequiredWith: []string{"password_version"}, + }, + }, + } +} + +func resourceWriteOnlyValidationsCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId("fakeid-123") + + passwordVal, diags := d.GetRawConfigAt(cty.GetAttrPath("writeonly_password")) + if diags.HasError() { + return diags + } + + if !passwordVal.IsNull() { + if passwordVal.AsString() != "newpassword" && passwordVal.AsString() != "newpassword2" { + return diag.Errorf("expected `writeonly_password` to be `newpassword` or `newpassword2`, got: %q", passwordVal.AsString()) + } + + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err := d.Set("writeonly_password", "different value") + if err != nil { + return diag.FromErr(err) + } + } else { + passwordVal := d.Get("old_password_attr").(string) + if passwordVal != "oldpassword" && passwordVal != "oldpassword2" { + return diag.Errorf("expected `old_password_attr` to be `oldpassword` or `oldpassword2`, got: %q", passwordVal) + } + } + + return nil +} + +func resourceWriteOnlyValidationsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Config isn't set for Read, so can't verify write-only data + return nil +} + +func resourceWriteOnlyValidationsUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Once created, the only operation that can occur is replacement (delete/create) + return nil +} + +func resourceWriteOnlyValidationsDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // Config isn't set for Delete, so can't verify write-only data + return nil +} diff --git a/internal/sdkv2provider/resource_write_only_validations_test.go b/internal/sdkv2provider/resource_write_only_validations_test.go new file mode 100644 index 0000000..cc41c50 --- /dev/null +++ b/internal/sdkv2provider/resource_write_only_validations_test.go @@ -0,0 +1,140 @@ +package sdkv2 + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +// +// This test flips between using a normal configured attribute (old_password_attr) and a write-only attribute +// with a configured version attribute (password_version, writeonly_password). +func TestWriteOnlyValidationsResource(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_validations" "test" {}`, + ExpectError: regexp.MustCompile(`Invalid combination of arguments`), + }, + { + // TODO: The testing framework can't verify warning diagnostics currently, although one would be returned here + // to indicate that the preferred attribute is "writeonly_password". This should only appear when the client supports write-only attributes. + // https://github.com/hashicorp/terraform-plugin-testing/issues/69 + Config: `resource "corner_writeonly_validations" "test" { + old_password_attr = "oldpassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_validations.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.StringExact("oldpassword")), + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + Config: `resource "corner_writeonly_validations" "test" { + password_version = "v1" + old_password_attr = "oldpassword" + }`, + ExpectError: regexp.MustCompile(`"old_password_attr": conflicts with password_version`), + }, + { + Config: `resource "corner_writeonly_validations" "test" { + writeonly_password = "newpassword" + }`, + ExpectError: regexp.MustCompile(`Missing required argument`), // password_version is needed to handle triggering the replacements + }, + { + // Replaces resource with new write-only attribute + version + Config: `resource "corner_writeonly_validations" "test" { + password_version = "v1" + writeonly_password = "newpassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + // No-op, as password_version did not change + Config: `resource "corner_writeonly_validations" "test" { + password_version = "v1" + writeonly_password = "won't trigger an update on it's own" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_validations.test", plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + Config: `resource "corner_writeonly_validations" "test" { + password_version = "v2" + }`, + ExpectError: regexp.MustCompile(`Missing required argument`), // writeonly_password is needed to set new password + }, + { + // Triggers replace with new password_version + Config: `resource "corner_writeonly_validations" "test" { + password_version = "v2" + writeonly_password = "newpassword2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + // Switching back to normal configured attribute which is stored in state + Config: `resource "corner_writeonly_validations" "test" { + old_password_attr = "oldpassword2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.StringExact("oldpassword2")), + statecheck.ExpectKnownValue("corner_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + }, + }) +} From 1e5bb882f592f2f8a50284d8283a32dbf04430ef Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 Jan 2025 10:19:25 -0500 Subject: [PATCH 19/35] testing state nulling for fw providers --- .../framework5provider/writeonly_resource.go | 17 +-- .../writeonly_resource_test.go | 73 +++++++++++ .../framework6provider/writeonly_resource.go | 17 +-- .../writeonly_resource_test.go | 118 ++++++++++++++++++ 4 files changed, 211 insertions(+), 14 deletions(-) diff --git a/internal/framework5provider/writeonly_resource.go b/internal/framework5provider/writeonly_resource.go index 940c627..6b8bee4 100644 --- a/internal/framework5provider/writeonly_resource.go +++ b/internal/framework5provider/writeonly_resource.go @@ -80,8 +80,8 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r } func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan WriteOnlyResourceModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } @@ -91,7 +91,9 @@ func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateReques return } - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) } func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -107,9 +109,8 @@ func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan WriteOnlyResourceModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } @@ -119,7 +120,9 @@ func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateReques return } - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) } func (r WriteOnlyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { diff --git a/internal/framework5provider/writeonly_resource_test.go b/internal/framework5provider/writeonly_resource_test.go index 778bff0..bba32e8 100644 --- a/internal/framework5provider/writeonly_resource_test.go +++ b/internal/framework5provider/writeonly_resource_test.go @@ -102,6 +102,79 @@ func TestWriteOnlyResource(t *testing.T) { })), }, }, + { + Config: `resource "framework_writeonly" "test" { + writeonly_custom_ipv6 = "::" + writeonly_string = "fakepassword" + writeonly_set = ["fake", "password"] + nested_block_list { + string_attr = "world" + writeonly_string = "fakepassword1" + + double_nested_object { + bool_attr = true + writeonly_bool = false + } + } + nested_block_list { + string_attr = "hello" + writeonly_string = "fakepassword2" + + double_nested_object { + bool_attr = false + writeonly_bool = true + } + } + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, }, }) } diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go index f4fd965..5a07a32 100644 --- a/internal/framework6provider/writeonly_resource.go +++ b/internal/framework6provider/writeonly_resource.go @@ -120,8 +120,8 @@ func (r WriteOnlyResource) Schema(_ context.Context, _ resource.SchemaRequest, r } func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan WriteOnlyResourceModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } @@ -131,7 +131,9 @@ func (r WriteOnlyResource) Create(ctx context.Context, req resource.CreateReques return } - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) } func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { @@ -147,9 +149,8 @@ func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, r } func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan WriteOnlyResourceModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + var config WriteOnlyResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) if resp.Diagnostics.HasError() { return } @@ -159,7 +160,9 @@ func (r WriteOnlyResource) Update(ctx context.Context, req resource.UpdateReques return } - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) } func (r WriteOnlyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { diff --git a/internal/framework6provider/writeonly_resource_test.go b/internal/framework6provider/writeonly_resource_test.go index 3d539c7..3043e48 100644 --- a/internal/framework6provider/writeonly_resource_test.go +++ b/internal/framework6provider/writeonly_resource_test.go @@ -147,6 +147,124 @@ func TestWriteOnlyResource(t *testing.T) { })), }, }, + { + Config: `resource "framework_writeonly" "test" { + writeonly_custom_ipv6 = "::" + writeonly_string = "fakepassword" + writeonly_set = ["fake", "password"] + writeonly_nested_object = { + writeonly_int64 = 1234 + writeonly_bool = true + + writeonly_nested_list = [ + { + writeonly_string = "fakepassword1" + }, + { + writeonly_string = "fakepassword2" + } + ] + } + nested_map = { + "key1": { + string_attr = "hello" + writeonly_float64 = 10 + } + "key2": { + string_attr = "world" + writeonly_float64 = 20 + } + } + nested_block_list { + string_attr = "world" + writeonly_string = "fakepassword1" + + double_nested_object { + bool_attr = true + writeonly_bool = false + } + } + nested_block_list { + string_attr = "hello" + writeonly_string = "fakepassword2" + + double_nested_object { + bool_attr = false + writeonly_bool = true + } + } + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_map"), knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_float64": knownvalue.Null(), + }), + "key2": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_float64": knownvalue.Null(), + }), + })), + plancheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_custom_ipv6"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_set"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("writeonly_nested_object"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_map"), knownvalue.MapExact(map[string]knownvalue.Check{ + "key1": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_float64": knownvalue.Null(), + }), + "key2": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_float64": knownvalue.Null(), + }), + })), + statecheck.ExpectKnownValue("framework_writeonly.test", tfjsonpath.New("nested_block_list"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("world"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(true), + "writeonly_bool": knownvalue.Null(), + }), + }), + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "string_attr": knownvalue.StringExact("hello"), + "writeonly_string": knownvalue.Null(), + "double_nested_object": knownvalue.ObjectExact(map[string]knownvalue.Check{ + "bool_attr": knownvalue.Bool(false), + "writeonly_bool": knownvalue.Null(), + }), + }), + })), + }, + }, }, }) } From 922e213bce02fb890f74c8e3c40c45b79c3958e4 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 Jan 2025 11:25:14 -0500 Subject: [PATCH 20/35] add write-once tests to framework providers --- .../string_required_on_create.go | 41 +++++ internal/framework5provider/provider.go | 1 + .../framework5provider/writeonce_resource.go | 92 ++++++++++ .../writeonce_resource_test.go | 157 ++++++++++++++++++ internal/framework6provider/provider.go | 1 + .../framework6provider/writeonce_resource.go | 92 ++++++++++ .../writeonce_resource_test.go | 157 ++++++++++++++++++ 7 files changed, 541 insertions(+) create mode 100644 internal/cornertesting/string_required_on_create.go create mode 100644 internal/framework5provider/writeonce_resource.go create mode 100644 internal/framework5provider/writeonce_resource_test.go create mode 100644 internal/framework6provider/writeonce_resource.go create mode 100644 internal/framework6provider/writeonce_resource_test.go diff --git a/internal/cornertesting/string_required_on_create.go b/internal/cornertesting/string_required_on_create.go new file mode 100644 index 0000000..f1505f2 --- /dev/null +++ b/internal/cornertesting/string_required_on_create.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package cornertesting + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +func RequiredOnCreate() planmodifier.String { + return requiredOnCreateModifier{} +} + +type requiredOnCreateModifier struct{} + +func (m requiredOnCreateModifier) Description(_ context.Context) string { + return "This attribute is required only when creating the resource." +} + +func (m requiredOnCreateModifier) MarkdownDescription(_ context.Context) string { + return "This attribute is required only when creating the resource." +} + +func (m requiredOnCreateModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + // If there is a non-null state, we are destroying or updating so no validation is needed + if !req.State.Raw.IsNull() { + return + } + + // We are creating, but the attribute value is not present in config, return an error. + if req.ConfigValue.IsNull() { + resp.Diagnostics.AddAttributeError( + req.Path, + "Attribute Required when Creating", + fmt.Sprintf("Must set a configuration value for the %s attribute when creating.", req.Path.String()), + ) + return + } +} diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index 507af23..39acc96 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -92,6 +92,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewMoveStateResource, NewSetNestedBlockWithDefaultsResource, NewWriteOnlyResource, + NewWriteOnceResource, } } diff --git a/internal/framework5provider/writeonce_resource.go b/internal/framework5provider/writeonce_resource.go new file mode 100644 index 0000000..86cbe0d --- /dev/null +++ b/internal/framework5provider/writeonce_resource.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-corner/internal/cornertesting" +) + +var _ resource.Resource = WriteOnceResource{} + +func NewWriteOnceResource() resource.Resource { + return &WriteOnceResource{} +} + +type WriteOnceResource struct{} + +func (r WriteOnceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonce" +} + +func (r WriteOnceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "trigger_attr": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + // The only place that validation can reference prior state (which is required to determine if the planned action is + // a create, i.e, prior state is null) is during plan modification. So the plan modifier implementation is responsible + // for applying the "write-once" validation + "writeonce_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + PlanModifiers: []planmodifier.String{ + cornertesting.RequiredOnCreate(), + }, + }, + }, + } +} + +func (r WriteOnceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnceResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(assertWriteOnlyVal(ctx, req.Config, path.Root("writeonce_string"), types.StringValue("fakepassword"))...) + if resp.Diagnostics.HasError() { + return + } + + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnceResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Once created, the only operation that can occur is replacement (delete/create) +} + +func (r WriteOnceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnceResourceModel struct { + TriggerAttr types.String `tfsdk:"trigger_attr"` + WriteOnlyString types.String `tfsdk:"writeonce_string"` +} diff --git a/internal/framework5provider/writeonce_resource_test.go b/internal/framework5provider/writeonce_resource_test.go new file mode 100644 index 0000000..9ebbfa3 --- /dev/null +++ b/internal/framework5provider/writeonce_resource_test.go @@ -0,0 +1,157 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +func TestWriteOnceResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + { + // Now that the resource is created, we can remove the attribute with no planned changes + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionNoop), + }, + }, + }, + { + // Write-only attributes cannot participate and plan as they will always be null in prior/proposed new state + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "this value cannot prompt a change on it's own" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionNoop), + }, + }, + }, + { + // trigger_attr will prompt the replace action here + Config: `resource "framework_writeonce" "test" { + trigger_attr = "2" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + }, + }) +} + +func TestWriteOnceResource_error_on_create(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // This error message should occur on all Terraform versions. + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ExpectError: regexp.MustCompile(`Attribute Required when Creating`), + }, + }, + }) +} + +func TestWriteOnceResource_error_on_replace(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionReplace), + }, + }, + ExpectError: regexp.MustCompile(`Attribute Required when Creating`), + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index a92aa66..1a6784d 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -92,6 +92,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewSetNestedBlockWithDefaultsResource, NewSetNestedAttributeWithDefaultsResource, NewWriteOnlyResource, + NewWriteOnceResource, } } diff --git a/internal/framework6provider/writeonce_resource.go b/internal/framework6provider/writeonce_resource.go new file mode 100644 index 0000000..86cbe0d --- /dev/null +++ b/internal/framework6provider/writeonce_resource.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-corner/internal/cornertesting" +) + +var _ resource.Resource = WriteOnceResource{} + +func NewWriteOnceResource() resource.Resource { + return &WriteOnceResource{} +} + +type WriteOnceResource struct{} + +func (r WriteOnceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonce" +} + +func (r WriteOnceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "trigger_attr": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + // The only place that validation can reference prior state (which is required to determine if the planned action is + // a create, i.e, prior state is null) is during plan modification. So the plan modifier implementation is responsible + // for applying the "write-once" validation + "writeonce_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + PlanModifiers: []planmodifier.String{ + cornertesting.RequiredOnCreate(), + }, + }, + }, + } +} + +func (r WriteOnceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnceResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(assertWriteOnlyVal(ctx, req.Config, path.Root("writeonce_string"), types.StringValue("fakepassword"))...) + if resp.Diagnostics.HasError() { + return + } + + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnceResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Once created, the only operation that can occur is replacement (delete/create) +} + +func (r WriteOnceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnceResourceModel struct { + TriggerAttr types.String `tfsdk:"trigger_attr"` + WriteOnlyString types.String `tfsdk:"writeonce_string"` +} diff --git a/internal/framework6provider/writeonce_resource_test.go b/internal/framework6provider/writeonce_resource_test.go new file mode 100644 index 0000000..32388bc --- /dev/null +++ b/internal/framework6provider/writeonce_resource_test.go @@ -0,0 +1,157 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +func TestWriteOnceResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + { + // Now that the resource is created, we can remove the attribute with no planned changes + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionNoop), + }, + }, + }, + { + // Write-only attributes cannot participate and plan as they will always be null in prior/proposed new state + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "this value cannot prompt a change on it's own" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionNoop), + }, + }, + }, + { + // trigger_attr will prompt the replace action here + Config: `resource "framework_writeonce" "test" { + trigger_attr = "2" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + }, + }) +} + +func TestWriteOnceResource_error_on_create(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // This error message should occur on all Terraform versions. + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ExpectError: regexp.MustCompile(`Attribute Required when Creating`), + }, + }, + }) +} + +func TestWriteOnceResource_error_on_replace(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "1" + writeonce_string = "fakepassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + }, + }, + { + Config: `resource "framework_writeonce" "test" { + trigger_attr = "2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonce.test", tfjsonpath.New("writeonce_string"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonce.test", plancheck.ResourceActionReplace), + }, + }, + ExpectError: regexp.MustCompile(`Attribute Required when Creating`), + }, + }, + }) +} From 3269cfa209f4d2a320ffd36afe00832c4b014b2c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 Jan 2025 16:42:03 -0500 Subject: [PATCH 21/35] add write-only validation tests for FW providers --- go.mod | 1 + go.sum | 2 + .../prefer_write_only_attribute.go | 73 +++++++++ internal/framework5provider/provider.go | 1 + .../writeonly_validations_resource.go | 138 +++++++++++++++++ .../writeonly_validations_resource_test.go | 142 ++++++++++++++++++ internal/framework6provider/provider.go | 1 + .../writeonly_validations_resource.go | 138 +++++++++++++++++ .../writeonly_validations_resource_test.go | 142 ++++++++++++++++++ 9 files changed, 638 insertions(+) create mode 100644 internal/cornertesting/prefer_write_only_attribute.go create mode 100644 internal/framework5provider/writeonly_validations_resource.go create mode 100644 internal/framework5provider/writeonly_validations_resource_test.go create mode 100644 internal/framework6provider/writeonly_validations_resource.go create mode 100644 internal/framework6provider/writeonly_validations_resource_test.go diff --git a/go.mod b/go.mod index b81190d..b8bb9f6 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.16.0 github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa github.com/hashicorp/terraform-plugin-mux v0.17.1-0.20241217174601-fdf2e5e009de github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.1-0.20250103201413-5f29273ad6f0 diff --git a/go.sum b/go.sum index 0c3ac8c..998e7b4 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,8 @@ github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaK github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 h1:v3DapR8gsp3EM8fKMh6up9cJUFQ2iRaFsYLP8UJnCco= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0/go.mod h1:c3PnGE9pHBDfdEVG9t1S1C9ia5LW+gkFR0CygXlM8ak= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.0 h1:O9QqGoYDzQT7lwTXUsZEtgabeWW96zUBh47Smn2lkFA= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.0/go.mod h1:Bh89/hNmqsEWug4/XWKYBwtnw3tbz5BAy1L1OgvbIaY= github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa h1:GOXZVYZrfDrWxZMHdSNqZKDwayH8WBBtyOLx25ekwv8= github.com/hashicorp/terraform-plugin-go v0.25.1-0.20241217173851-dcf8f64dbfaa/go.mod h1:OKJU8uauqiLVRWjlFB0KIgK++baq26qfvOU1IVycx9k= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= diff --git a/internal/cornertesting/prefer_write_only_attribute.go b/internal/cornertesting/prefer_write_only_attribute.go new file mode 100644 index 0000000..6ad7e35 --- /dev/null +++ b/internal/cornertesting/prefer_write_only_attribute.go @@ -0,0 +1,73 @@ +package cornertesting + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func PreferWriteOnlyAttribute(oldAttribute, newAttribute path.Expression) resource.ConfigValidator { + return preferWriteOnlyAttributeValidator{ + oldAttribute: oldAttribute, + newAttribute: newAttribute, + } +} + +var _ resource.ConfigValidator = &preferWriteOnlyAttributeValidator{} + +type preferWriteOnlyAttributeValidator struct { + oldAttribute path.Expression + newAttribute path.Expression +} + +func (v preferWriteOnlyAttributeValidator) Description(ctx context.Context) string { + return v.MarkdownDescription(ctx) +} + +func (v preferWriteOnlyAttributeValidator) MarkdownDescription(_ context.Context) string { + return fmt.Sprintf("If the client supports write-only attributes (Terraform v1.11+), attribute %s should be used in-place of %s", v.newAttribute, v.oldAttribute) +} + +func (v preferWriteOnlyAttributeValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + // Write-only attributes are not supported in the client, so no warning should be raised. + if !req.ClientCapabilities.WriteOnlyAttributesAllowed { + return + } + + matchedOldPaths, matchedOldPathsDiags := req.Config.PathMatches(ctx, v.oldAttribute) + resp.Diagnostics.Append(matchedOldPathsDiags...) + if resp.Diagnostics.HasError() { + return + } + + var diags diag.Diagnostics + for _, matchedOldPath := range matchedOldPaths { + var value attr.Value + getAttributeDiags := req.Config.GetAttribute(ctx, matchedOldPath, &value) + + diags.Append(getAttributeDiags...) + + // Collect all errors + if getAttributeDiags.HasError() { + continue + } + + // Value must not be null or unknown to trigger validation error + if value.IsNull() || value.IsUnknown() { + continue + } + + diags.AddAttributeWarning( + matchedOldPath, + "Available Write-Only Attribute Alternative", + fmt.Sprintf("The attribute %s has a WriteOnly version %s available. "+ + "Use the WriteOnly version of the attribute when possible.", matchedOldPath, v.newAttribute), + ) + } + + resp.Diagnostics = diags +} diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index 39acc96..ef93416 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -93,6 +93,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewSetNestedBlockWithDefaultsResource, NewWriteOnlyResource, NewWriteOnceResource, + NewWriteOnlyValidationsResource, } } diff --git a/internal/framework5provider/writeonly_validations_resource.go b/internal/framework5provider/writeonly_validations_resource.go new file mode 100644 index 0000000..97bda13 --- /dev/null +++ b/internal/framework5provider/writeonly_validations_resource.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-corner/internal/cornertesting" +) + +var _ resource.Resource = WriteOnlyValidationsResource{} +var _ resource.ResourceWithConfigValidators = WriteOnlyValidationsResource{} + +func NewWriteOnlyValidationsResource() resource.Resource { + return &WriteOnlyValidationsResource{} +} + +type WriteOnlyValidationsResource struct{} + +func (r WriteOnlyValidationsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_validations" +} + +func (r WriteOnlyValidationsResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + // TODO: Eventually, this should be replaced when we create a similar validator in terraform-plugin-framework-validators + cornertesting.PreferWriteOnlyAttribute( + path.MatchRoot("old_password_attr"), + path.MatchRoot("writeonly_password"), + ), + resourcevalidator.Conflicting( + path.MatchRoot("old_password_attr"), + path.MatchRoot("password_version"), + ), + resourcevalidator.ExactlyOneOf( + path.MatchRoot("old_password_attr"), + path.MatchRoot("writeonly_password"), + ), + resourcevalidator.RequiredTogether( + path.MatchRoot("password_version"), + path.MatchRoot("writeonly_password"), + ), + } +} + +func (r WriteOnlyValidationsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "old_password_attr": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("password_version")), + }, + }, + "password_version": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "writeonly_password": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyValidationsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyValidationsResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + if !config.WriteOnlyPassword.IsNull() { + if config.WriteOnlyPassword.ValueString() != "newpassword" && config.WriteOnlyPassword.ValueString() != "newpassword2" { + resp.Diagnostics.AddAttributeError( + path.Root("writeonly_password"), + "Unexpected writeonly_password value", + fmt.Sprintf("expected `writeonly_password` to be `newpassword` or `newpassword2`, got: %q", config.WriteOnlyPassword.ValueString()), + ) + return + } + } else { + if config.OldPasswordAttr.ValueString() != "oldpassword" && config.OldPasswordAttr.ValueString() != "oldpassword2" { + resp.Diagnostics.AddAttributeError( + path.Root("old_password_attr"), + "Unexpected old_password_attr value", + fmt.Sprintf("expected `old_password_attr` to be `oldpassword` or `oldpassword2`, got: %q", config.OldPasswordAttr.ValueString()), + ) + return + } + } + + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyValidationsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyValidationsResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyValidationsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Once created, the only operation that can occur is replacement (delete/create) +} + +func (r WriteOnlyValidationsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnlyValidationsResourceModel struct { + OldPasswordAttr types.String `tfsdk:"old_password_attr"` + PasswordVersion types.String `tfsdk:"password_version"` + WriteOnlyPassword types.String `tfsdk:"writeonly_password"` +} diff --git a/internal/framework5provider/writeonly_validations_resource_test.go b/internal/framework5provider/writeonly_validations_resource_test.go new file mode 100644 index 0000000..efdbea8 --- /dev/null +++ b/internal/framework5provider/writeonly_validations_resource_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +func TestWriteOnlyValidationsResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly_validations" "test" {}`, + ExpectError: regexp.MustCompile(`Missing Attribute Configuration`), + }, + { + // TODO: The testing framework can't verify warning diagnostics currently, although one would be returned here + // to indicate that the preferred attribute is "writeonly_password". This should only appear when the client supports write-only attributes. + // https://github.com/hashicorp/terraform-plugin-testing/issues/69 + Config: `resource "framework_writeonly_validations" "test" { + old_password_attr = "oldpassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.StringExact("oldpassword")), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v1" + old_password_attr = "oldpassword" + }`, + ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), + }, + { + Config: `resource "framework_writeonly_validations" "test" { + writeonly_password = "newpassword" + }`, + ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), // password_version is needed to handle triggering the replacements + }, + { + // Replaces resource with new write-only attribute + version + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v1" + writeonly_password = "newpassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + // No-op, as password_version did not change + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v1" + writeonly_password = "won't trigger an update on it's own" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v2" + }`, + ExpectError: regexp.MustCompile(`Missing Attribute Configuration`), // writeonly_password is needed to set new password + }, + { + // Triggers replace with new password_version + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v2" + writeonly_password = "newpassword2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + // Switching back to normal configured attribute which is stored in state + Config: `resource "framework_writeonly_validations" "test" { + old_password_attr = "oldpassword2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.StringExact("oldpassword2")), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index 1a6784d..cf5cd37 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -93,6 +93,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewSetNestedAttributeWithDefaultsResource, NewWriteOnlyResource, NewWriteOnceResource, + NewWriteOnlyValidationsResource, } } diff --git a/internal/framework6provider/writeonly_validations_resource.go b/internal/framework6provider/writeonly_validations_resource.go new file mode 100644 index 0000000..97bda13 --- /dev/null +++ b/internal/framework6provider/writeonly_validations_resource.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-corner/internal/cornertesting" +) + +var _ resource.Resource = WriteOnlyValidationsResource{} +var _ resource.ResourceWithConfigValidators = WriteOnlyValidationsResource{} + +func NewWriteOnlyValidationsResource() resource.Resource { + return &WriteOnlyValidationsResource{} +} + +type WriteOnlyValidationsResource struct{} + +func (r WriteOnlyValidationsResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_validations" +} + +func (r WriteOnlyValidationsResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + // TODO: Eventually, this should be replaced when we create a similar validator in terraform-plugin-framework-validators + cornertesting.PreferWriteOnlyAttribute( + path.MatchRoot("old_password_attr"), + path.MatchRoot("writeonly_password"), + ), + resourcevalidator.Conflicting( + path.MatchRoot("old_password_attr"), + path.MatchRoot("password_version"), + ), + resourcevalidator.ExactlyOneOf( + path.MatchRoot("old_password_attr"), + path.MatchRoot("writeonly_password"), + ), + resourcevalidator.RequiredTogether( + path.MatchRoot("password_version"), + path.MatchRoot("writeonly_password"), + ), + } +} + +func (r WriteOnlyValidationsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "old_password_attr": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("password_version")), + }, + }, + "password_version": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "writeonly_password": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyValidationsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyValidationsResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + if !config.WriteOnlyPassword.IsNull() { + if config.WriteOnlyPassword.ValueString() != "newpassword" && config.WriteOnlyPassword.ValueString() != "newpassword2" { + resp.Diagnostics.AddAttributeError( + path.Root("writeonly_password"), + "Unexpected writeonly_password value", + fmt.Sprintf("expected `writeonly_password` to be `newpassword` or `newpassword2`, got: %q", config.WriteOnlyPassword.ValueString()), + ) + return + } + } else { + if config.OldPasswordAttr.ValueString() != "oldpassword" && config.OldPasswordAttr.ValueString() != "oldpassword2" { + resp.Diagnostics.AddAttributeError( + path.Root("old_password_attr"), + "Unexpected old_password_attr value", + fmt.Sprintf("expected `old_password_attr` to be `oldpassword` or `oldpassword2`, got: %q", config.OldPasswordAttr.ValueString()), + ) + return + } + } + + // Since all attributes are in configuration, we write it back directly to test that the write-only attributes + // are nulled out before sending back to TF Core. + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyValidationsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyValidationsResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyValidationsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Once created, the only operation that can occur is replacement (delete/create) +} + +func (r WriteOnlyValidationsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnlyValidationsResourceModel struct { + OldPasswordAttr types.String `tfsdk:"old_password_attr"` + PasswordVersion types.String `tfsdk:"password_version"` + WriteOnlyPassword types.String `tfsdk:"writeonly_password"` +} diff --git a/internal/framework6provider/writeonly_validations_resource_test.go b/internal/framework6provider/writeonly_validations_resource_test.go new file mode 100644 index 0000000..83d374b --- /dev/null +++ b/internal/framework6provider/writeonly_validations_resource_test.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// MAINTAINER NOTE: All the write-only data in these tests are hardcoded in the resource itself to verify +// the config data is passed to the resource Create function. +func TestWriteOnlyValidationsResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly_validations" "test" {}`, + ExpectError: regexp.MustCompile(`Missing Attribute Configuration`), + }, + { + // TODO: The testing framework can't verify warning diagnostics currently, although one would be returned here + // to indicate that the preferred attribute is "writeonly_password". This should only appear when the client supports write-only attributes. + // https://github.com/hashicorp/terraform-plugin-testing/issues/69 + Config: `resource "framework_writeonly_validations" "test" { + old_password_attr = "oldpassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.StringExact("oldpassword")), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v1" + old_password_attr = "oldpassword" + }`, + ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), + }, + { + Config: `resource "framework_writeonly_validations" "test" { + writeonly_password = "newpassword" + }`, + ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), // password_version is needed to handle triggering the replacements + }, + { + // Replaces resource with new write-only attribute + version + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v1" + writeonly_password = "newpassword" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + // No-op, as password_version did not change + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v1" + writeonly_password = "won't trigger an update on it's own" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v2" + }`, + ExpectError: regexp.MustCompile(`Missing Attribute Configuration`), // writeonly_password is needed to set new password + }, + { + // Triggers replace with new password_version + Config: `resource "framework_writeonly_validations" "test" { + password_version = "v2" + writeonly_password = "newpassword2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.Null()), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + { + // Switching back to normal configured attribute which is stored in state + Config: `resource "framework_writeonly_validations" "test" { + old_password_attr = "oldpassword2" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + plancheck.ExpectResourceAction("framework_writeonly_validations.test", plancheck.ResourceActionReplace), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("old_password_attr"), knownvalue.StringExact("oldpassword2")), + statecheck.ExpectKnownValue("framework_writeonly_validations.test", tfjsonpath.New("writeonly_password"), knownvalue.Null()), + }, + }, + }, + }) +} From 0b7a3de890913238932e914c2d6e4956d3861b5c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 Jan 2025 18:30:44 -0500 Subject: [PATCH 22/35] update copywrite --- internal/cornertesting/prefer_write_only_attribute.go | 3 +++ internal/sdkv2provider/resource_write_once_test.go | 3 +++ internal/sdkv2provider/resource_write_only_test.go | 3 +++ internal/sdkv2provider/resource_write_only_validations_test.go | 3 +++ 4 files changed, 12 insertions(+) diff --git a/internal/cornertesting/prefer_write_only_attribute.go b/internal/cornertesting/prefer_write_only_attribute.go index 6ad7e35..6fe8ff6 100644 --- a/internal/cornertesting/prefer_write_only_attribute.go +++ b/internal/cornertesting/prefer_write_only_attribute.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package cornertesting import ( diff --git a/internal/sdkv2provider/resource_write_once_test.go b/internal/sdkv2provider/resource_write_once_test.go index 1cc9d96..89ae280 100644 --- a/internal/sdkv2provider/resource_write_once_test.go +++ b/internal/sdkv2provider/resource_write_once_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package sdkv2 import ( diff --git a/internal/sdkv2provider/resource_write_only_test.go b/internal/sdkv2provider/resource_write_only_test.go index 2d86cff..70d3ca3 100644 --- a/internal/sdkv2provider/resource_write_only_test.go +++ b/internal/sdkv2provider/resource_write_only_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package sdkv2 import ( diff --git a/internal/sdkv2provider/resource_write_only_validations_test.go b/internal/sdkv2provider/resource_write_only_validations_test.go index cc41c50..040cc7e 100644 --- a/internal/sdkv2provider/resource_write_only_validations_test.go +++ b/internal/sdkv2provider/resource_write_only_validations_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package sdkv2 import ( From b30fd01888fbf3748fa3df06fda9b1e29d72a198 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 Jan 2025 18:41:30 -0500 Subject: [PATCH 23/35] add data check tests --- internal/protocolprovider/server.go | 37 ++ .../protocolprovider/writeonly_datacheck.go | 323 ++++++++++++++++++ .../writeonly_datacheck_test.go | 163 +++++++++ .../writeonly_legacy_datacheck_test.go | 101 ++++++ 4 files changed, 624 insertions(+) create mode 100644 internal/protocolprovider/writeonly_datacheck.go create mode 100644 internal/protocolprovider/writeonly_datacheck_test.go create mode 100644 internal/protocolprovider/writeonly_legacy_datacheck_test.go diff --git a/internal/protocolprovider/server.go b/internal/protocolprovider/server.go index f50bf54..c189cbf 100644 --- a/internal/protocolprovider/server.go +++ b/internal/protocolprovider/server.go @@ -145,5 +145,42 @@ func Server() tfprotov5.ProviderServer { functionRouter: functionRouter{ "bool": functionBool{}, }, + resourceSchemas: map[string]*tfprotov5.Schema{ + "corner_writeonly_datacheck": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_readerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_legacy_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_legacy_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), + }, + resourceRouter: resourceRouter{ + "corner_writeonly_datacheck": resourceWriteOnlyDataCheck{}, + "corner_writeonly_datacheck_planerror": resourceWriteOnlyDataCheck{ + planDataError: true, + }, + "corner_writeonly_datacheck_applyerror": resourceWriteOnlyDataCheck{ + applyDataError: true, + }, + "corner_writeonly_datacheck_readerror": resourceWriteOnlyDataCheck{ + readDataError: true, + }, + "corner_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{ + importDataError: true, + }, + "corner_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{ + enableLegacyTypeSystem: true, + }, + // MAINTAINER NOTE: The only RPCs that have legacy type system flags are plan/apply + "corner_writeonly_legacy_datacheck_planerror": resourceWriteOnlyDataCheck{ + enableLegacyTypeSystem: true, + planDataError: true, + }, + "corner_writeonly_legacy_datacheck_applyerror": resourceWriteOnlyDataCheck{ + enableLegacyTypeSystem: true, + applyDataError: true, + }, + }, } } diff --git a/internal/protocolprovider/writeonly_datacheck.go b/internal/protocolprovider/writeonly_datacheck.go new file mode 100644 index 0000000..0b74c2c --- /dev/null +++ b/internal/protocolprovider/writeonly_datacheck.go @@ -0,0 +1,323 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package protocol + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type resourceWriteOnlyDataCheck struct { + enableLegacyTypeSystem bool + applyDataError bool + planDataError bool + readDataError bool + importDataError bool +} + +func (r resourceWriteOnlyDataCheck) schema() *tfprotov5.Schema { + return &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + // Only used for import testing + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "writeonly_attr", + Type: tftypes.String, + Required: true, + WriteOnly: true, + }, + }, + }, + } +} + +// nonNullWriteOnlyData is used to produce data which will raise an error diagnostic in Terraform core. +func (r resourceWriteOnlyDataCheck) nonNullWriteOnlyData() (tfprotov5.DynamicValue, error) { + return tfprotov5.NewDynamicValue( + r.schema().ValueType(), + tftypes.NewValue( + r.schema().ValueType(), + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-123"), + "writeonly_attr": tftypes.NewValue(tftypes.String, "this should cause an error!"), + }, + ), + ) +} + +// nullWriteOnlyData is used to produce valid data. +func (r resourceWriteOnlyDataCheck) nullWriteOnlyData() (tfprotov5.DynamicValue, error) { + return tfprotov5.NewDynamicValue( + r.schema().ValueType(), + tftypes.NewValue( + r.schema().ValueType(), + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-123"), + "writeonly_attr": tftypes.NewValue(tftypes.String, nil), + }, + ), + ) +} + +func (r resourceWriteOnlyDataCheck) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { + plannedState, diag := dynamicValueToValue(r.schema(), req.PlannedState) + if diag != nil { + return &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{diag}, + }, nil + } + + // Destroy Op, just return planned state, which is null + if plannedState.IsNull() { + return &tfprotov5.ApplyResourceChangeResponse{ + NewState: req.PlannedState, + }, nil + } + + var newState tfprotov5.DynamicValue + var err error + if r.applyDataError { + newState, err = r.nonNullWriteOnlyData() + } else { + newState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error encoding new state", + Detail: fmt.Sprintf("Error encoding new state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov5.ApplyResourceChangeResponse{ + NewState: &newState, + UnsafeToUseLegacyTypeSystem: r.enableLegacyTypeSystem, + }, nil +} + +func (r resourceWriteOnlyDataCheck) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { + proposedNewState, diag := dynamicValueToValue(r.schema(), req.ProposedNewState) + if diag != nil { + return &tfprotov5.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{diag}, + }, nil + } + + // Destroying the resource, just return proposed new state (which is null) + if proposedNewState.IsNull() { + return &tfprotov5.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + }, nil + } + + var plannedState tfprotov5.DynamicValue + var err error + if r.planDataError { + plannedState, err = r.nonNullWriteOnlyData() + } else { + plannedState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov5.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error encoding planned state", + Detail: fmt.Sprintf("Error encoding planned state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &plannedState, + UnsafeToUseLegacyTypeSystem: r.enableLegacyTypeSystem, + }, nil +} + +func (r resourceWriteOnlyDataCheck) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { + var newState tfprotov5.DynamicValue + var err error + if r.readDataError { + newState, err = r.nonNullWriteOnlyData() + } else { + newState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov5.ReadResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error encoding new state", + Detail: fmt.Sprintf("Error encoding new state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov5.ReadResourceResponse{ + NewState: &newState, + }, nil +} + +func (r resourceWriteOnlyDataCheck) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { + return &tfprotov5.ValidateResourceTypeConfigResponse{}, nil +} + +func (r resourceWriteOnlyDataCheck) ImportResourceState(ctx context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { + + var importedState tfprotov5.DynamicValue + var err error + if r.importDataError { + importedState, err = r.nonNullWriteOnlyData() + } else { + importedState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error encoding import state", + Detail: fmt.Sprintf("Error encoding import state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + TypeName: req.TypeName, + State: &importedState, + }, + }, + }, nil +} + +func (r resourceWriteOnlyDataCheck) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { + resp := &tfprotov5.UpgradeResourceStateResponse{} + // Define options to be used when unmarshalling raw state. + // IgnoreUndefinedAttributes will silently skip over fields in the JSON + // that do not have a matching entry in the schema. + unmarshalOpts := tfprotov5.UnmarshalOpts{ + ValueFromJSONOpts: tftypes.ValueFromJSONOpts{ + IgnoreUndefinedAttributes: true, + }, + } + + // Terraform CLI can call UpgradeResourceState even if the stored state + // version matches the current schema. Presumably this is to account for + // the previous terraform-plugin-sdk implementation, which handled some + // state fixups on behalf of Terraform CLI. This will attempt to roundtrip + // the prior RawState to a state matching the current schema. + rawStateValue, err := req.RawState.UnmarshalWithOpts(r.schema().ValueType(), unmarshalOpts) + + if err != nil { + diag := &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Read Previously Saved State for UpgradeResourceState", + Detail: "There was an error reading the saved resource state using the current resource schema: " + err.Error(), + } + + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil //nolint:nilerr // error via diagnostic, not gRPC + } + + upgradedState, diag := valuetoDynamicValue(r.schema(), rawStateValue) + + if diag != nil { + resp.Diagnostics = append(resp.Diagnostics, diag) + + return resp, nil + } + + resp.UpgradedState = upgradedState + + return resp, nil +} + +func (r resourceWriteOnlyDataCheck) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) { + return &tfprotov5.MoveResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unsupported Resource Operation", + Detail: "MoveResourceState is not supported by this resource.", + }, + }, + }, nil +} + +func valuetoDynamicValue(schema *tfprotov5.Schema, value tftypes.Value) (*tfprotov5.DynamicValue, *tfprotov5.Diagnostic) { + if schema == nil { + diag := &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: missing schema", + } + + return nil, diag + } + + dynamicValue, err := tfprotov5.NewDynamicValue(schema.ValueType(), value) + if err != nil { + diag := &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Value", + Detail: "Converting the Value to DynamicValue returned an unexpected error: " + err.Error(), + } + + return &dynamicValue, diag + } + + return &dynamicValue, nil +} + +func dynamicValueToValue(schema *tfprotov5.Schema, dynamicValue *tfprotov5.DynamicValue) (tftypes.Value, *tfprotov5.Diagnostic) { + if schema == nil { + diag := &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: missing schema", + } + + return tftypes.NewValue(tftypes.Object{}, nil), diag + } + + if dynamicValue == nil { + return tftypes.NewValue(schema.ValueType(), nil), nil + } + + value, err := dynamicValue.Unmarshal(schema.ValueType()) + + if err != nil { + diag := &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: " + err.Error(), + } + + return value, diag + } + + return value, nil +} diff --git a/internal/protocolprovider/writeonly_datacheck_test.go b/internal/protocolprovider/writeonly_datacheck_test.go new file mode 100644 index 0000000..ed1a9a8 --- /dev/null +++ b/internal/protocolprovider/writeonly_datacheck_test.go @@ -0,0 +1,163 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package protocol + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAccResourceWriteOnlyDataCheck_success(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_datacheck" "test" { + writeonly_attr = "hello world!" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_datacheck.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + }, + }, + { + ResourceName: "corner_writeonly_datacheck.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_plan_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_datacheck_planerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_apply_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_datacheck_applyerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_read_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_datacheck_readerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_import_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_datacheck_importerror" "test" { + writeonly_attr = "hello world!" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_datacheck_importerror.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_datacheck_importerror.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_datacheck_importerror.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + }, + }, + { + ResourceName: "corner_writeonly_datacheck_importerror.test", + ImportState: true, + ImportStateVerify: true, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} diff --git a/internal/protocolprovider/writeonly_legacy_datacheck_test.go b/internal/protocolprovider/writeonly_legacy_datacheck_test.go new file mode 100644 index 0000000..4878ea6 --- /dev/null +++ b/internal/protocolprovider/writeonly_legacy_datacheck_test.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package protocol + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAccResourceWriteOnlyLegacyDataCheck_success(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_legacy_datacheck" "test" { + writeonly_attr = "hello world!" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_writeonly_legacy_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_writeonly_legacy_datacheck.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_legacy_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + }, + }, + { + ResourceName: "corner_writeonly_legacy_datacheck.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceWriteOnlyLegacyDataCheck_plan_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_legacy_datacheck_planerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyLegacyDataCheck_apply_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_legacy_datacheck_applyerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} From d33be2e2c69f73a77f9a1091a0d9780dfe529b1b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 7 Jan 2025 09:51:59 -0500 Subject: [PATCH 24/35] move resource state test --- internal/protocolprovider/resource_router.go | 15 ++----- internal/protocolprovider/server.go | 5 +++ .../protocolprovider/writeonly_datacheck.go | 41 +++++++++++++++---- .../writeonly_datacheck_test.go | 39 ++++++++++++++++++ 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/internal/protocolprovider/resource_router.go b/internal/protocolprovider/resource_router.go index 41f0e75..869af42 100644 --- a/internal/protocolprovider/resource_router.go +++ b/internal/protocolprovider/resource_router.go @@ -66,19 +66,10 @@ func (r resourceRouter) ImportResourceState(ctx context.Context, req *tfprotov5. } func (r resourceRouter) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) { - _, ok := r[req.TargetTypeName] + res, ok := r[req.TargetTypeName] if !ok { return nil, errUnsupportedResource(req.TargetTypeName) } - // If this support ever needs to be added, this can follow the existing - // pattern of calling res.MoveResourceState(ctx, req). - return &tfprotov5.MoveResourceStateResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Unsupported Resource Operation", - Detail: "MoveResourceState is not supported by this provider.", - }, - }, - }, nil + + return res.MoveResourceState(ctx, req) } diff --git a/internal/protocolprovider/server.go b/internal/protocolprovider/server.go index c189cbf..3e94a44 100644 --- a/internal/protocolprovider/server.go +++ b/internal/protocolprovider/server.go @@ -30,6 +30,7 @@ type server struct { func (s *server) serverCapabilities() *tfprotov5.ServerCapabilities { return &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, } } @@ -151,6 +152,7 @@ func Server() tfprotov5.ProviderServer { "corner_writeonly_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), "corner_writeonly_datacheck_readerror": resourceWriteOnlyDataCheck{}.schema(), "corner_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_moveresourceerror": resourceWriteOnlyDataCheck{}.schema(), "corner_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{}.schema(), "corner_writeonly_legacy_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), "corner_writeonly_legacy_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), @@ -169,6 +171,9 @@ func Server() tfprotov5.ProviderServer { "corner_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{ importDataError: true, }, + "corner_writeonly_datacheck_moveresourceerror": resourceWriteOnlyDataCheck{ + moveResourceDataError: true, + }, "corner_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{ enableLegacyTypeSystem: true, }, diff --git a/internal/protocolprovider/writeonly_datacheck.go b/internal/protocolprovider/writeonly_datacheck.go index 0b74c2c..9f557c7 100644 --- a/internal/protocolprovider/writeonly_datacheck.go +++ b/internal/protocolprovider/writeonly_datacheck.go @@ -17,6 +17,7 @@ type resourceWriteOnlyDataCheck struct { planDataError bool readDataError bool importDataError bool + moveResourceDataError bool } func (r resourceWriteOnlyDataCheck) schema() *tfprotov5.Schema { @@ -256,14 +257,40 @@ func (r resourceWriteOnlyDataCheck) UpgradeResourceState(ctx context.Context, re } func (r resourceWriteOnlyDataCheck) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) { - return &tfprotov5.MoveResourceStateResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Unsupported Resource Operation", - Detail: "MoveResourceState is not supported by this resource.", + if req.SourceProviderAddress != "terraform.io/builtin/terraform" || req.SourceTypeName != "terraform_data" { + return &tfprotov5.MoveResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unsupported MoveResourceState Operation", + Detail: `Move operations for this resource are only supported from the "terraform.io/builtin/terraform" provider and the "terraform_data" resource type.`, + }, }, - }, + }, nil + } + + var moveResourceState tfprotov5.DynamicValue + var err error + if r.moveResourceDataError { + moveResourceState, err = r.nonNullWriteOnlyData() + } else { + moveResourceState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov5.MoveResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error encoding moved state", + Detail: fmt.Sprintf("Error encoding moved state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov5.MoveResourceStateResponse{ + TargetState: &moveResourceState, }, nil } diff --git a/internal/protocolprovider/writeonly_datacheck_test.go b/internal/protocolprovider/writeonly_datacheck_test.go index ed1a9a8..efa7f75 100644 --- a/internal/protocolprovider/writeonly_datacheck_test.go +++ b/internal/protocolprovider/writeonly_datacheck_test.go @@ -161,3 +161,42 @@ func TestAccResourceWriteOnlyDataCheck_import_error(t *testing.T) { }, }) } + +func TestAccResourceWriteOnlyDataCheck_moveresource_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + { + Config: `resource "corner_writeonly_datacheck_moveresourceerror" "test" { + writeonly_attr = "hello world!" + } + + moved { + from = terraform_data.test + to = corner_writeonly_datacheck_moveresourceerror.test + }`, + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + // Back to the original config to avoid a destroy clean-up error. + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + }, + }) +} From 26d1f0e1cb4d39ec34d54d91d867f9cba19c7846 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 7 Jan 2025 11:57:02 -0500 Subject: [PATCH 25/35] comment --- internal/protocolprovider/writeonly_datacheck_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/protocolprovider/writeonly_datacheck_test.go b/internal/protocolprovider/writeonly_datacheck_test.go index efa7f75..6e1b7e4 100644 --- a/internal/protocolprovider/writeonly_datacheck_test.go +++ b/internal/protocolprovider/writeonly_datacheck_test.go @@ -189,6 +189,7 @@ func TestAccResourceWriteOnlyDataCheck_moveresource_error(t *testing.T) { from = terraform_data.test to = corner_writeonly_datacheck_moveresourceerror.test }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), }, // Back to the original config to avoid a destroy clean-up error. From ef4cd5ee47d58b1f9b5b1ad5fc48e41648f21c15 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 7 Jan 2025 17:59:56 -0500 Subject: [PATCH 26/35] upgrade resource state bugged test --- internal/protocolprovider/server.go | 22 ++-- .../protocolprovider/writeonly_datacheck.go | 101 +++++++----------- .../writeonly_datacheck_test.go | 26 +++++ 3 files changed, 75 insertions(+), 74 deletions(-) diff --git a/internal/protocolprovider/server.go b/internal/protocolprovider/server.go index 3e94a44..8381801 100644 --- a/internal/protocolprovider/server.go +++ b/internal/protocolprovider/server.go @@ -147,15 +147,16 @@ func Server() tfprotov5.ProviderServer { "bool": functionBool{}, }, resourceSchemas: map[string]*tfprotov5.Schema{ - "corner_writeonly_datacheck": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_datacheck_readerror": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_datacheck_moveresourceerror": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_legacy_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), - "corner_writeonly_legacy_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_readerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_moveresourceerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_datacheck_upgraderesourceerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_legacy_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_writeonly_legacy_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), }, resourceRouter: resourceRouter{ "corner_writeonly_datacheck": resourceWriteOnlyDataCheck{}, @@ -174,6 +175,9 @@ func Server() tfprotov5.ProviderServer { "corner_writeonly_datacheck_moveresourceerror": resourceWriteOnlyDataCheck{ moveResourceDataError: true, }, + "corner_writeonly_datacheck_upgraderesourceerror": resourceWriteOnlyDataCheck{ + upgradeResourceDataError: true, + }, "corner_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{ enableLegacyTypeSystem: true, }, diff --git a/internal/protocolprovider/writeonly_datacheck.go b/internal/protocolprovider/writeonly_datacheck.go index 9f557c7..1dc72d5 100644 --- a/internal/protocolprovider/writeonly_datacheck.go +++ b/internal/protocolprovider/writeonly_datacheck.go @@ -12,12 +12,13 @@ import ( ) type resourceWriteOnlyDataCheck struct { - enableLegacyTypeSystem bool - applyDataError bool - planDataError bool - readDataError bool - importDataError bool - moveResourceDataError bool + enableLegacyTypeSystem bool + applyDataError bool + planDataError bool + readDataError bool + importDataError bool + moveResourceDataError bool + upgradeResourceDataError bool } func (r resourceWriteOnlyDataCheck) schema() *tfprotov5.Schema { @@ -214,46 +215,41 @@ func (r resourceWriteOnlyDataCheck) ImportResourceState(ctx context.Context, req } func (r resourceWriteOnlyDataCheck) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { - resp := &tfprotov5.UpgradeResourceStateResponse{} - // Define options to be used when unmarshalling raw state. - // IgnoreUndefinedAttributes will silently skip over fields in the JSON - // that do not have a matching entry in the schema. - unmarshalOpts := tfprotov5.UnmarshalOpts{ - ValueFromJSONOpts: tftypes.ValueFromJSONOpts{ - IgnoreUndefinedAttributes: true, - }, + if req.Version != 0 { + return &tfprotov5.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unsupported UpgradeResourceState Operation", + Detail: fmt.Sprintf(`Unexpected version upgrade, there is only version 0 of the resource. Received upgrade request with version %d`, req.Version), + }, + }, + }, nil } - // Terraform CLI can call UpgradeResourceState even if the stored state - // version matches the current schema. Presumably this is to account for - // the previous terraform-plugin-sdk implementation, which handled some - // state fixups on behalf of Terraform CLI. This will attempt to roundtrip - // the prior RawState to a state matching the current schema. - rawStateValue, err := req.RawState.UnmarshalWithOpts(r.schema().ValueType(), unmarshalOpts) - - if err != nil { - diag := &tfprotov5.Diagnostic{ - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Unable to Read Previously Saved State for UpgradeResourceState", - Detail: "There was an error reading the saved resource state using the current resource schema: " + err.Error(), - } - - resp.Diagnostics = append(resp.Diagnostics, diag) - - return resp, nil //nolint:nilerr // error via diagnostic, not gRPC + var upgradeResourceState tfprotov5.DynamicValue + var err error + if r.upgradeResourceDataError { + upgradeResourceState, err = r.nonNullWriteOnlyData() + } else { + upgradeResourceState, err = r.nullWriteOnlyData() } - upgradedState, diag := valuetoDynamicValue(r.schema(), rawStateValue) - - if diag != nil { - resp.Diagnostics = append(resp.Diagnostics, diag) - - return resp, nil + if err != nil { + return &tfprotov5.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error encoding upgraded state", + Detail: fmt.Sprintf("Error encoding upgraded state: %s", err.Error()), + }, + }, + }, nil } - resp.UpgradedState = upgradedState - - return resp, nil + return &tfprotov5.UpgradeResourceStateResponse{ + UpgradedState: &upgradeResourceState, + }, nil } func (r resourceWriteOnlyDataCheck) MoveResourceState(ctx context.Context, req *tfprotov5.MoveResourceStateRequest) (*tfprotov5.MoveResourceStateResponse, error) { @@ -294,31 +290,6 @@ func (r resourceWriteOnlyDataCheck) MoveResourceState(ctx context.Context, req * }, nil } -func valuetoDynamicValue(schema *tfprotov5.Schema, value tftypes.Value) (*tfprotov5.DynamicValue, *tfprotov5.Diagnostic) { - if schema == nil { - diag := &tfprotov5.Diagnostic{ - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Unable to Convert Value", - Detail: "Converting the Value to DynamicValue returned an unexpected error: missing schema", - } - - return nil, diag - } - - dynamicValue, err := tfprotov5.NewDynamicValue(schema.ValueType(), value) - if err != nil { - diag := &tfprotov5.Diagnostic{ - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Unable to Convert Value", - Detail: "Converting the Value to DynamicValue returned an unexpected error: " + err.Error(), - } - - return &dynamicValue, diag - } - - return &dynamicValue, nil -} - func dynamicValueToValue(schema *tfprotov5.Schema, dynamicValue *tfprotov5.DynamicValue) (tftypes.Value, *tfprotov5.Diagnostic) { if schema == nil { diag := &tfprotov5.Diagnostic{ diff --git a/internal/protocolprovider/writeonly_datacheck_test.go b/internal/protocolprovider/writeonly_datacheck_test.go index 6e1b7e4..703865e 100644 --- a/internal/protocolprovider/writeonly_datacheck_test.go +++ b/internal/protocolprovider/writeonly_datacheck_test.go @@ -162,6 +162,32 @@ func TestAccResourceWriteOnlyDataCheck_import_error(t *testing.T) { }) } +func TestAccResourceWriteOnlyDataCheck_upgraderesource_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov5.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_datacheck_upgraderesourceerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This test is currently bugged because UpgradeResourceState TF core is not returning errors for non-null W/O values. + // This should be uncommented when fixed. + // ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`After applying this test step, the non-refresh plan was not empty.`), + }, + }, + }) +} + func TestAccResourceWriteOnlyDataCheck_moveresource_error(t *testing.T) { resource.UnitTest(t, resource.TestCase{ // Write-only attributes are only available in 1.11.0+ From 4358d6b42a6a9379fc2c02bc801c6607acd12f86 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 7 Jan 2025 18:00:27 -0500 Subject: [PATCH 27/35] comment --- internal/protocolprovider/writeonly_datacheck_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/protocolprovider/writeonly_datacheck_test.go b/internal/protocolprovider/writeonly_datacheck_test.go index 703865e..07d66aa 100644 --- a/internal/protocolprovider/writeonly_datacheck_test.go +++ b/internal/protocolprovider/writeonly_datacheck_test.go @@ -180,7 +180,7 @@ func TestAccResourceWriteOnlyDataCheck_upgraderesource_error(t *testing.T) { writeonly_attr = "hello world!" }`, // TODO: This test is currently bugged because UpgradeResourceState TF core is not returning errors for non-null W/O values. - // This should be uncommented when fixed. + // This should be uncommented when fixed: https://hashicorp.slack.com/archives/C071HC4JJCC/p1736289662267609 // ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), ExpectError: regexp.MustCompile(`After applying this test step, the non-refresh plan was not empty.`), }, From 688c526347eea97ed3e979cf23559f5a8ae9b156 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 8 Jan 2025 08:52:57 -0500 Subject: [PATCH 28/35] protocolv6 data checks --- .../protocolv6provider/resource_router.go | 15 +- internal/protocolv6provider/server.go | 46 +++ .../protocolv6provider/writeonly_datacheck.go | 321 ++++++++++++++++++ .../writeonly_datacheck_test.go | 229 +++++++++++++ .../writeonly_legacy_datacheck_test.go | 101 ++++++ 5 files changed, 700 insertions(+), 12 deletions(-) create mode 100644 internal/protocolv6provider/writeonly_datacheck.go create mode 100644 internal/protocolv6provider/writeonly_datacheck_test.go create mode 100644 internal/protocolv6provider/writeonly_legacy_datacheck_test.go diff --git a/internal/protocolv6provider/resource_router.go b/internal/protocolv6provider/resource_router.go index de3a0d7..bdd36be 100644 --- a/internal/protocolv6provider/resource_router.go +++ b/internal/protocolv6provider/resource_router.go @@ -66,19 +66,10 @@ func (r resourceRouter) ImportResourceState(ctx context.Context, req *tfprotov6. } func (r resourceRouter) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { - _, ok := r[req.TargetTypeName] + res, ok := r[req.TargetTypeName] if !ok { return nil, errUnsupportedResource(req.TargetTypeName) } - // If this support ever needs to be added, this can follow the existing - // pattern of calling res.MoveResourceState(ctx, req). - return &tfprotov6.MoveResourceStateResponse{ - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "Unsupported Resource Operation", - Detail: "MoveResourceState is not supported by this provider.", - }, - }, - }, nil + + return res.MoveResourceState(ctx, req) } diff --git a/internal/protocolv6provider/server.go b/internal/protocolv6provider/server.go index acc4ecc..8881afb 100644 --- a/internal/protocolv6provider/server.go +++ b/internal/protocolv6provider/server.go @@ -30,6 +30,7 @@ type server struct { func (s *server) serverCapabilities() *tfprotov6.ServerCapabilities { return &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, + MoveResourceState: true, } } @@ -145,5 +146,50 @@ func Server() tfprotov6.ProviderServer { functionRouter: functionRouter{ "bool": functionBool{}, }, + resourceSchemas: map[string]*tfprotov6.Schema{ + "corner_v6_writeonly_datacheck": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_datacheck_readerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_datacheck_moveresourceerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_datacheck_upgraderesourceerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_legacy_datacheck_planerror": resourceWriteOnlyDataCheck{}.schema(), + "corner_v6_writeonly_legacy_datacheck_applyerror": resourceWriteOnlyDataCheck{}.schema(), + }, + resourceRouter: resourceRouter{ + "corner_v6_writeonly_datacheck": resourceWriteOnlyDataCheck{}, + "corner_v6_writeonly_datacheck_planerror": resourceWriteOnlyDataCheck{ + planDataError: true, + }, + "corner_v6_writeonly_datacheck_applyerror": resourceWriteOnlyDataCheck{ + applyDataError: true, + }, + "corner_v6_writeonly_datacheck_readerror": resourceWriteOnlyDataCheck{ + readDataError: true, + }, + "corner_v6_writeonly_datacheck_importerror": resourceWriteOnlyDataCheck{ + importDataError: true, + }, + "corner_v6_writeonly_datacheck_moveresourceerror": resourceWriteOnlyDataCheck{ + moveResourceDataError: true, + }, + "corner_v6_writeonly_datacheck_upgraderesourceerror": resourceWriteOnlyDataCheck{ + upgradeResourceDataError: true, + }, + "corner_v6_writeonly_legacy_datacheck": resourceWriteOnlyDataCheck{ + enableLegacyTypeSystem: true, + }, + // MAINTAINER NOTE: The only RPCs that have legacy type system flags are plan/apply + "corner_v6_writeonly_legacy_datacheck_planerror": resourceWriteOnlyDataCheck{ + enableLegacyTypeSystem: true, + planDataError: true, + }, + "corner_v6_writeonly_legacy_datacheck_applyerror": resourceWriteOnlyDataCheck{ + enableLegacyTypeSystem: true, + applyDataError: true, + }, + }, } } diff --git a/internal/protocolv6provider/writeonly_datacheck.go b/internal/protocolv6provider/writeonly_datacheck.go new file mode 100644 index 0000000..f483db9 --- /dev/null +++ b/internal/protocolv6provider/writeonly_datacheck.go @@ -0,0 +1,321 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package protocolv6 + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type resourceWriteOnlyDataCheck struct { + enableLegacyTypeSystem bool + applyDataError bool + planDataError bool + readDataError bool + importDataError bool + moveResourceDataError bool + upgradeResourceDataError bool +} + +func (r resourceWriteOnlyDataCheck) schema() *tfprotov6.Schema { + return &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + // Only used for import testing + Name: "id", + Type: tftypes.String, + Computed: true, + }, + { + Name: "writeonly_attr", + Type: tftypes.String, + Required: true, + WriteOnly: true, + }, + }, + }, + } +} + +// nonNullWriteOnlyData is used to produce data which will raise an error diagnostic in Terraform core. +func (r resourceWriteOnlyDataCheck) nonNullWriteOnlyData() (tfprotov6.DynamicValue, error) { + return tfprotov6.NewDynamicValue( + r.schema().ValueType(), + tftypes.NewValue( + r.schema().ValueType(), + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-123"), + "writeonly_attr": tftypes.NewValue(tftypes.String, "this should cause an error!"), + }, + ), + ) +} + +// nullWriteOnlyData is used to produce valid data. +func (r resourceWriteOnlyDataCheck) nullWriteOnlyData() (tfprotov6.DynamicValue, error) { + return tfprotov6.NewDynamicValue( + r.schema().ValueType(), + tftypes.NewValue( + r.schema().ValueType(), + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-123"), + "writeonly_attr": tftypes.NewValue(tftypes.String, nil), + }, + ), + ) +} + +func (r resourceWriteOnlyDataCheck) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { + plannedState, diag := dynamicValueToValue(r.schema(), req.PlannedState) + if diag != nil { + return &tfprotov6.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{diag}, + }, nil + } + + // Destroy Op, just return planned state, which is null + if plannedState.IsNull() { + return &tfprotov6.ApplyResourceChangeResponse{ + NewState: req.PlannedState, + }, nil + } + + var newState tfprotov6.DynamicValue + var err error + if r.applyDataError { + newState, err = r.nonNullWriteOnlyData() + } else { + newState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov6.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error encoding new state", + Detail: fmt.Sprintf("Error encoding new state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov6.ApplyResourceChangeResponse{ + NewState: &newState, + UnsafeToUseLegacyTypeSystem: r.enableLegacyTypeSystem, + }, nil +} + +func (r resourceWriteOnlyDataCheck) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { + proposedNewState, diag := dynamicValueToValue(r.schema(), req.ProposedNewState) + if diag != nil { + return &tfprotov6.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{diag}, + }, nil + } + + // Destroying the resource, just return proposed new state (which is null) + if proposedNewState.IsNull() { + return &tfprotov6.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + }, nil + } + + var plannedState tfprotov6.DynamicValue + var err error + if r.planDataError { + plannedState, err = r.nonNullWriteOnlyData() + } else { + plannedState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov6.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error encoding planned state", + Detail: fmt.Sprintf("Error encoding planned state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov6.PlanResourceChangeResponse{ + PlannedState: &plannedState, + UnsafeToUseLegacyTypeSystem: r.enableLegacyTypeSystem, + }, nil +} + +func (r resourceWriteOnlyDataCheck) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { + var newState tfprotov6.DynamicValue + var err error + if r.readDataError { + newState, err = r.nonNullWriteOnlyData() + } else { + newState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov6.ReadResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error encoding new state", + Detail: fmt.Sprintf("Error encoding new state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov6.ReadResourceResponse{ + NewState: &newState, + }, nil +} + +func (r resourceWriteOnlyDataCheck) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { + return &tfprotov6.ValidateResourceConfigResponse{}, nil +} + +func (r resourceWriteOnlyDataCheck) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { + + var importedState tfprotov6.DynamicValue + var err error + if r.importDataError { + importedState, err = r.nonNullWriteOnlyData() + } else { + importedState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov6.ImportResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error encoding import state", + Detail: fmt.Sprintf("Error encoding import state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov6.ImportResourceStateResponse{ + ImportedResources: []*tfprotov6.ImportedResource{ + { + TypeName: req.TypeName, + State: &importedState, + }, + }, + }, nil +} + +func (r resourceWriteOnlyDataCheck) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { + if req.Version != 0 { + return &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported UpgradeResourceState Operation", + Detail: fmt.Sprintf(`Unexpected version upgrade, there is only version 0 of the resource. Received upgrade request with version %d`, req.Version), + }, + }, + }, nil + } + + var upgradeResourceState tfprotov6.DynamicValue + var err error + if r.upgradeResourceDataError { + upgradeResourceState, err = r.nonNullWriteOnlyData() + } else { + upgradeResourceState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error encoding upgraded state", + Detail: fmt.Sprintf("Error encoding upgraded state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov6.UpgradeResourceStateResponse{ + UpgradedState: &upgradeResourceState, + }, nil +} + +func (r resourceWriteOnlyDataCheck) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) { + if req.SourceProviderAddress != "terraform.io/builtin/terraform" || req.SourceTypeName != "terraform_data" { + return &tfprotov6.MoveResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unsupported MoveResourceState Operation", + Detail: `Move operations for this resource are only supported from the "terraform.io/builtin/terraform" provider and the "terraform_data" resource type.`, + }, + }, + }, nil + } + + var moveResourceState tfprotov6.DynamicValue + var err error + if r.moveResourceDataError { + moveResourceState, err = r.nonNullWriteOnlyData() + } else { + moveResourceState, err = r.nullWriteOnlyData() + } + + if err != nil { + return &tfprotov6.MoveResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error encoding moved state", + Detail: fmt.Sprintf("Error encoding moved state: %s", err.Error()), + }, + }, + }, nil + } + + return &tfprotov6.MoveResourceStateResponse{ + TargetState: &moveResourceState, + }, nil +} + +func dynamicValueToValue(schema *tfprotov6.Schema, dynamicValue *tfprotov6.DynamicValue) (tftypes.Value, *tfprotov6.Diagnostic) { + if schema == nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: missing schema", + } + + return tftypes.NewValue(tftypes.Object{}, nil), diag + } + + if dynamicValue == nil { + return tftypes.NewValue(schema.ValueType(), nil), nil + } + + value, err := dynamicValue.Unmarshal(schema.ValueType()) + + if err != nil { + diag := &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert DynamicValue", + Detail: "Converting the DynamicValue to Value returned an unexpected error: " + err.Error(), + } + + return value, diag + } + + return value, nil +} diff --git a/internal/protocolv6provider/writeonly_datacheck_test.go b/internal/protocolv6provider/writeonly_datacheck_test.go new file mode 100644 index 0000000..be21422 --- /dev/null +++ b/internal/protocolv6provider/writeonly_datacheck_test.go @@ -0,0 +1,229 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package protocolv6 + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAccResourceWriteOnlyDataCheck_success(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_datacheck" "test" { + writeonly_attr = "hello world!" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_v6_writeonly_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_v6_writeonly_datacheck.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_v6_writeonly_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + }, + }, + { + ResourceName: "corner_v6_writeonly_datacheck.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_plan_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_datacheck_planerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_apply_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_datacheck_applyerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_read_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_datacheck_readerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_import_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_datacheck_importerror" "test" { + writeonly_attr = "hello world!" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_v6_writeonly_datacheck_importerror.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_v6_writeonly_datacheck_importerror.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_v6_writeonly_datacheck_importerror.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + }, + }, + { + ResourceName: "corner_v6_writeonly_datacheck_importerror.test", + ImportState: true, + ImportStateVerify: true, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_upgraderesource_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_datacheck_upgraderesourceerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This test is currently bugged because UpgradeResourceState TF core is not returning errors for non-null W/O values. + // This should be uncommented when fixed: https://hashicorp.slack.com/archives/C071HC4JJCC/p1736289662267609 + // ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`After applying this test step, the non-refresh plan was not empty.`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyDataCheck_moveresource_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + { + Config: `resource "corner_v6_writeonly_datacheck_moveresourceerror" "test" { + writeonly_attr = "hello world!" + } + + moved { + from = terraform_data.test + to = corner_v6_writeonly_datacheck_moveresourceerror.test + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + // Back to the original config to avoid a destroy clean-up error. + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + }, + }) +} diff --git a/internal/protocolv6provider/writeonly_legacy_datacheck_test.go b/internal/protocolv6provider/writeonly_legacy_datacheck_test.go new file mode 100644 index 0000000..0eb96fa --- /dev/null +++ b/internal/protocolv6provider/writeonly_legacy_datacheck_test.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package protocolv6 + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAccResourceWriteOnlyLegacyDataCheck_success(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_legacy_datacheck" "test" { + writeonly_attr = "hello world!" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue("corner_v6_writeonly_legacy_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + plancheck.ExpectResourceAction("corner_v6_writeonly_legacy_datacheck.test", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_v6_writeonly_legacy_datacheck.test", tfjsonpath.New("writeonly_attr"), knownvalue.Null()), + }, + }, + { + ResourceName: "corner_v6_writeonly_legacy_datacheck.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceWriteOnlyLegacyDataCheck_plan_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_legacy_datacheck_planerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} + +func TestAccResourceWriteOnlyLegacyDataCheck_apply_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + //nolint:unparam // False positive in unparam related to map: https://github.com/mvdan/unparam/issues/40 + "corner": func() (tfprotov6.ProviderServer, error) { + return Server(), nil + }, + }, + Steps: []resource.TestStep{ + { + Config: `resource "corner_v6_writeonly_legacy_datacheck_applyerror" "test" { + writeonly_attr = "hello world!" + }`, + // TODO: This error message will likely be changed to be more specific before 1.11 GA + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} From 0a2580ba25bd1d6e094e9a94a7c50c9498084e76 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 Jan 2025 13:55:26 -0500 Subject: [PATCH 29/35] requires_replace temp example --- internal/framework6provider/provider.go | 1 + .../write_only_replace_ex.go | 82 +++++++++++++++++++ .../write_only_replace_ex_test.go | 64 +++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 internal/framework6provider/write_only_replace_ex.go create mode 100644 internal/framework6provider/write_only_replace_ex_test.go diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index cf5cd37..160b2e2 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -94,6 +94,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewWriteOnlyResource, NewWriteOnceResource, NewWriteOnlyValidationsResource, + NewWriteOnlyReplaceExResource, } } diff --git a/internal/framework6provider/write_only_replace_ex.go b/internal/framework6provider/write_only_replace_ex.go new file mode 100644 index 0000000..a2d71b5 --- /dev/null +++ b/internal/framework6provider/write_only_replace_ex.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyReplaceExResource{} + +func NewWriteOnlyReplaceExResource() resource.Resource { + return &WriteOnlyReplaceExResource{} +} + +type WriteOnlyReplaceExResource struct{} + +func (r WriteOnlyReplaceExResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_replace_ex" +} + +func (r WriteOnlyReplaceExResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Required: true, + WriteOnly: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), // This will end up in `requires_replace` always being populated with "writeonly_string" on non-create plans + }, + }, + }, + } +} + +func (r WriteOnlyReplaceExResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyReplaceExResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyReplaceExResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyReplaceExResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyReplaceExResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var config WriteOnlyReplaceExResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyReplaceExResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnlyReplaceExResourceModel struct { + StringAttr types.String `tfsdk:"string_attr"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` +} diff --git a/internal/framework6provider/write_only_replace_ex_test.go b/internal/framework6provider/write_only_replace_ex_test.go new file mode 100644 index 0000000..01607fe --- /dev/null +++ b/internal/framework6provider/write_only_replace_ex_test.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyReplaceExResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly_replace_ex" "test" { + string_attr = "hello!" + writeonly_string = "write-only value" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + // Create + plancheck.ExpectResourceAction("framework_writeonly_replace_ex.test", plancheck.ResourceActionCreate), + }, + }, + }, + { + Config: `resource "framework_writeonly_replace_ex" "test" { + string_attr = "hello!" + writeonly_string = "write-only value" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + // No-op, however, requires_replace is populated with a path to "writeonly_string" + plancheck.ExpectResourceAction("framework_writeonly_replace_ex.test", plancheck.ResourceActionNoop), + }, + }, + }, + { + Config: `resource "framework_writeonly_replace_ex" "test" { + string_attr = "goodbye!" + writeonly_string = "write-only value" + }`, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + // Update, however, requires_replace is populated with a path to "writeonly_string" + plancheck.ExpectResourceAction("framework_writeonly_replace_ex.test", plancheck.ResourceActionUpdate), + }, + }, + }, + }, + }) +} From 04075d76e7b41b2411714f1a004efdb6d365516f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 Jan 2025 14:38:22 -0500 Subject: [PATCH 30/35] remove replace example and add import for sdkv2 --- internal/framework6provider/provider.go | 1 - .../write_only_replace_ex.go | 82 ------------------- .../write_only_replace_ex_test.go | 64 --------------- internal/sdkv2provider/provider.go | 1 + .../resource_write_only_import.go | 80 ++++++++++++++++++ .../resource_write_only_import_test.go | 39 +++++++++ 6 files changed, 120 insertions(+), 147 deletions(-) delete mode 100644 internal/framework6provider/write_only_replace_ex.go delete mode 100644 internal/framework6provider/write_only_replace_ex_test.go create mode 100644 internal/sdkv2provider/resource_write_only_import.go create mode 100644 internal/sdkv2provider/resource_write_only_import_test.go diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index 160b2e2..cf5cd37 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -94,7 +94,6 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewWriteOnlyResource, NewWriteOnceResource, NewWriteOnlyValidationsResource, - NewWriteOnlyReplaceExResource, } } diff --git a/internal/framework6provider/write_only_replace_ex.go b/internal/framework6provider/write_only_replace_ex.go deleted file mode 100644 index a2d71b5..0000000 --- a/internal/framework6provider/write_only_replace_ex.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -var _ resource.Resource = WriteOnlyReplaceExResource{} - -func NewWriteOnlyReplaceExResource() resource.Resource { - return &WriteOnlyReplaceExResource{} -} - -type WriteOnlyReplaceExResource struct{} - -func (r WriteOnlyReplaceExResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_writeonly_replace_ex" -} - -func (r WriteOnlyReplaceExResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "string_attr": schema.StringAttribute{ - Required: true, - }, - "writeonly_string": schema.StringAttribute{ - Required: true, - WriteOnly: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), // This will end up in `requires_replace` always being populated with "writeonly_string" on non-create plans - }, - }, - }, - } -} - -func (r WriteOnlyReplaceExResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var config WriteOnlyReplaceExResourceModel - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) -} - -func (r WriteOnlyReplaceExResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data WriteOnlyReplaceExResourceModel - - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - -func (r WriteOnlyReplaceExResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var config WriteOnlyReplaceExResourceModel - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) -} - -func (r WriteOnlyReplaceExResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { -} - -type WriteOnlyReplaceExResourceModel struct { - StringAttr types.String `tfsdk:"string_attr"` - WriteOnlyString types.String `tfsdk:"writeonly_string"` -} diff --git a/internal/framework6provider/write_only_replace_ex_test.go b/internal/framework6provider/write_only_replace_ex_test.go deleted file mode 100644 index 01607fe..0000000 --- a/internal/framework6provider/write_only_replace_ex_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package framework - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-framework/providerserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" - "github.com/hashicorp/terraform-plugin-testing/tfversion" -) - -func TestWriteOnlyReplaceExResource(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - // Write-only attributes are only available in 1.11.0+ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_11_0), - }, - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "framework": providerserver.NewProtocol6WithError(New()), - }, - Steps: []resource.TestStep{ - { - Config: `resource "framework_writeonly_replace_ex" "test" { - string_attr = "hello!" - writeonly_string = "write-only value" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - // Create - plancheck.ExpectResourceAction("framework_writeonly_replace_ex.test", plancheck.ResourceActionCreate), - }, - }, - }, - { - Config: `resource "framework_writeonly_replace_ex" "test" { - string_attr = "hello!" - writeonly_string = "write-only value" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - // No-op, however, requires_replace is populated with a path to "writeonly_string" - plancheck.ExpectResourceAction("framework_writeonly_replace_ex.test", plancheck.ResourceActionNoop), - }, - }, - }, - { - Config: `resource "framework_writeonly_replace_ex" "test" { - string_attr = "goodbye!" - writeonly_string = "write-only value" - }`, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - // Update, however, requires_replace is populated with a path to "writeonly_string" - plancheck.ExpectResourceAction("framework_writeonly_replace_ex.test", plancheck.ResourceActionUpdate), - }, - }, - }, - }, - }) -} diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index 6c622a9..6c60c22 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -28,6 +28,7 @@ func New() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "corner_user": resourceUser(), "corner_writeonly": resourceWriteOnly(), + "corner_writeonly_import": resourceWriteOnlyImport(), "corner_writeonce": resourceWriteOnce(), "corner_writeonly_validations": resourceWriteOnlyValidations(), "corner_bigint": resourceBigint(), diff --git a/internal/sdkv2provider/resource_write_only_import.go b/internal/sdkv2provider/resource_write_only_import.go new file mode 100644 index 0000000..93cdcb9 --- /dev/null +++ b/internal/sdkv2provider/resource_write_only_import.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//nolint:forcetypeassert // Test SDK provider +package sdkv2 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceWriteOnlyImport() *schema.Resource { + return &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + + CreateContext: resourceWriteOnlyImportCreate, + ReadContext: resourceWriteOnlyImportRead, + UpdateContext: resourceWriteOnlyImportUpdate, + DeleteContext: resourceWriteOnlyImportDelete, + + Importer: &schema.ResourceImporter{ + StateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err := d.Set("writeonly_string", "different value") + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "string_attr": { + Type: schema.TypeString, + Required: true, + }, + "writeonly_string": { + Type: schema.TypeString, + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func resourceWriteOnlyImportCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId("fakeid-123") + + return nil +} + +func resourceWriteOnlyImportRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + err := d.Set("string_attr", "hello world!") + if err != nil { + return diag.FromErr(err) + } + + // Setting shouldn't result in anything sent back to Terraform, but we want to test that + // our SDKv2 logic would revert these changes. + err = d.Set("writeonly_string", "different value") + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceWriteOnlyImportUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceWriteOnlyImportDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} diff --git a/internal/sdkv2provider/resource_write_only_import_test.go b/internal/sdkv2provider/resource_write_only_import_test.go new file mode 100644 index 0000000..7f644d8 --- /dev/null +++ b/internal/sdkv2provider/resource_write_only_import_test.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package sdkv2 + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyImportResource(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: `resource "corner_writeonly_import" "test" { + string_attr = "hello world!" + writeonly_string = "fakepassword" + }`, + }, + { + ResourceName: "corner_writeonly_import.test", + ImportState: true, + ImportStateVerify: true, + // TODO: Remove this expect error once SDKv2 is updated to null out write-only attributes. + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} From ac8f6370ed50857aba9d7db3d40bb68fbea987b6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 Jan 2025 14:50:01 -0500 Subject: [PATCH 31/35] add TODO comment with readresource test --- internal/framework5provider/writeonly_resource.go | 3 +++ internal/framework6provider/writeonly_resource.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/internal/framework5provider/writeonly_resource.go b/internal/framework5provider/writeonly_resource.go index 6b8bee4..ea1c1ea 100644 --- a/internal/framework5provider/writeonly_resource.go +++ b/internal/framework5provider/writeonly_resource.go @@ -105,6 +105,9 @@ func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, r return } + // TODO: uncomment this once framework starts null-ing out write-only data in ReadResource. + // data.WriteOnlyString = types.StringValue("this shouldn't cause an error!") + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/framework6provider/writeonly_resource.go b/internal/framework6provider/writeonly_resource.go index 5a07a32..5daa66d 100644 --- a/internal/framework6provider/writeonly_resource.go +++ b/internal/framework6provider/writeonly_resource.go @@ -145,6 +145,9 @@ func (r WriteOnlyResource) Read(ctx context.Context, req resource.ReadRequest, r return } + // TODO: uncomment this once framework starts null-ing out write-only data in ReadResource. + // data.WriteOnlyString = types.StringValue("this shouldn't cause an error!") + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } From 58fc2422415a2ca1411c5878c3c30e665145d3b3 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 Jan 2025 17:19:14 -0500 Subject: [PATCH 32/35] import and move state fw tests --- internal/framework5provider/provider.go | 2 + .../writeonly_import_resource.go | 82 ++++++++++++++++++ .../writeonly_import_resource_test.go | 41 +++++++++ .../writeonly_move_resource.go | 84 +++++++++++++++++++ .../writeonly_move_resource_test.go | 53 ++++++++++++ internal/framework6provider/provider.go | 2 + .../writeonly_import_resource.go | 82 ++++++++++++++++++ .../writeonly_import_resource_test.go | 41 +++++++++ .../writeonly_move_resource.go | 84 +++++++++++++++++++ .../writeonly_move_resource_test.go | 53 ++++++++++++ 10 files changed, 524 insertions(+) create mode 100644 internal/framework5provider/writeonly_import_resource.go create mode 100644 internal/framework5provider/writeonly_import_resource_test.go create mode 100644 internal/framework5provider/writeonly_move_resource.go create mode 100644 internal/framework5provider/writeonly_move_resource_test.go create mode 100644 internal/framework6provider/writeonly_import_resource.go create mode 100644 internal/framework6provider/writeonly_import_resource_test.go create mode 100644 internal/framework6provider/writeonly_move_resource.go create mode 100644 internal/framework6provider/writeonly_move_resource_test.go diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index ef93416..928671a 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -94,6 +94,8 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewWriteOnlyResource, NewWriteOnceResource, NewWriteOnlyValidationsResource, + NewWriteOnlyImportResource, + NewWriteOnlyMoveResource, } } diff --git a/internal/framework5provider/writeonly_import_resource.go b/internal/framework5provider/writeonly_import_resource.go new file mode 100644 index 0000000..9d15127 --- /dev/null +++ b/internal/framework5provider/writeonly_import_resource.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyImportResource{} +var _ resource.ResourceWithImportState = WriteOnlyImportResource{} + +func NewWriteOnlyImportResource() resource.Resource { + return &WriteOnlyImportResource{} +} + +type WriteOnlyImportResource struct{} + +func (r WriteOnlyImportResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_import" +} + +func (r WriteOnlyImportResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyImportResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyImportResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyImportResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyImportResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + data.StringAttr = types.StringValue("hello world!") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyImportResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r WriteOnlyImportResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +func (r WriteOnlyImportResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute( + ctx, + path.Root("writeonly_string"), + types.StringValue("this shouldn't cause an error!"), + )...) +} + +type WriteOnlyImportResourceModel struct { + StringAttr types.String `tfsdk:"string_attr"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` +} diff --git a/internal/framework5provider/writeonly_import_resource_test.go b/internal/framework5provider/writeonly_import_resource_test.go new file mode 100644 index 0000000..5bcd055 --- /dev/null +++ b/internal/framework5provider/writeonly_import_resource_test.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyImportResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly_import" "test" { + string_attr = "hello world!" + writeonly_string = "fakepassword" + }`, + }, + { + ResourceName: "framework_writeonly_import.test", + ImportState: true, + ImportStateVerify: true, + // TODO: Remove this expect error once Framework is updated to null out write-only attributes. + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} diff --git a/internal/framework5provider/writeonly_move_resource.go b/internal/framework5provider/writeonly_move_resource.go new file mode 100644 index 0000000..37bf8ee --- /dev/null +++ b/internal/framework5provider/writeonly_move_resource.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyMoveResource{} +var _ resource.ResourceWithMoveState = WriteOnlyMoveResource{} + +func NewWriteOnlyMoveResource() resource.Resource { + return &WriteOnlyMoveResource{} +} + +type WriteOnlyMoveResource struct{} + +func (r WriteOnlyMoveResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_move" +} + +func (r WriteOnlyMoveResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyMoveResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyMoveResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyMoveResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyMoveResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyMoveResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r WriteOnlyMoveResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +func (r WriteOnlyMoveResource) MoveState(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + resp.Diagnostics.Append(resp.TargetState.Set(ctx, WriteOnlyMoveResourceModel{ + StringAttr: types.StringValue("hello world!"), + WriteOnlyString: types.StringValue("this shouldn't cause an error"), + })...) + }, + }, + } +} + +type WriteOnlyMoveResourceModel struct { + StringAttr types.String `tfsdk:"string_attr"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` +} diff --git a/internal/framework5provider/writeonly_move_resource_test.go b/internal/framework5provider/writeonly_move_resource_test.go new file mode 100644 index 0000000..70f5ff3 --- /dev/null +++ b/internal/framework5provider/writeonly_move_resource_test.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyMoveResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + { + Config: `resource "framework_writeonly_move" "test" { + string_attr = "hello world!" + writeonly_string = "fakepassword" + } + + moved { + from = terraform_data.test + to = framework_writeonly_move.test + }`, + // TODO: Remove this expect error once Framework is updated to null out write-only attributes. + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + // TODO: Remove this additional step once Framework is updated to null out write-only attributes. + // Back to the original config to avoid a destroy clean-up error. + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index cf5cd37..a67fe7e 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -94,6 +94,8 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewWriteOnlyResource, NewWriteOnceResource, NewWriteOnlyValidationsResource, + NewWriteOnlyImportResource, + NewWriteOnlyMoveResource, } } diff --git a/internal/framework6provider/writeonly_import_resource.go b/internal/framework6provider/writeonly_import_resource.go new file mode 100644 index 0000000..9d15127 --- /dev/null +++ b/internal/framework6provider/writeonly_import_resource.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyImportResource{} +var _ resource.ResourceWithImportState = WriteOnlyImportResource{} + +func NewWriteOnlyImportResource() resource.Resource { + return &WriteOnlyImportResource{} +} + +type WriteOnlyImportResource struct{} + +func (r WriteOnlyImportResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_import" +} + +func (r WriteOnlyImportResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyImportResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyImportResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyImportResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyImportResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + data.StringAttr = types.StringValue("hello world!") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyImportResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r WriteOnlyImportResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +func (r WriteOnlyImportResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.State.SetAttribute( + ctx, + path.Root("writeonly_string"), + types.StringValue("this shouldn't cause an error!"), + )...) +} + +type WriteOnlyImportResourceModel struct { + StringAttr types.String `tfsdk:"string_attr"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` +} diff --git a/internal/framework6provider/writeonly_import_resource_test.go b/internal/framework6provider/writeonly_import_resource_test.go new file mode 100644 index 0000000..f4fdf69 --- /dev/null +++ b/internal/framework6provider/writeonly_import_resource_test.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyImportResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_writeonly_import" "test" { + string_attr = "hello world!" + writeonly_string = "fakepassword" + }`, + }, + { + ResourceName: "framework_writeonly_import.test", + ImportState: true, + ImportStateVerify: true, + // TODO: Remove this expect error once Framework is updated to null out write-only attributes. + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + }, + }) +} diff --git a/internal/framework6provider/writeonly_move_resource.go b/internal/framework6provider/writeonly_move_resource.go new file mode 100644 index 0000000..37bf8ee --- /dev/null +++ b/internal/framework6provider/writeonly_move_resource.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyMoveResource{} +var _ resource.ResourceWithMoveState = WriteOnlyMoveResource{} + +func NewWriteOnlyMoveResource() resource.Resource { + return &WriteOnlyMoveResource{} +} + +type WriteOnlyMoveResource struct{} + +func (r WriteOnlyMoveResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_move" +} + +func (r WriteOnlyMoveResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyMoveResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyMoveResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyMoveResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyMoveResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyMoveResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r WriteOnlyMoveResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +func (r WriteOnlyMoveResource) MoveState(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + resp.Diagnostics.Append(resp.TargetState.Set(ctx, WriteOnlyMoveResourceModel{ + StringAttr: types.StringValue("hello world!"), + WriteOnlyString: types.StringValue("this shouldn't cause an error"), + })...) + }, + }, + } +} + +type WriteOnlyMoveResourceModel struct { + StringAttr types.String `tfsdk:"string_attr"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` +} diff --git a/internal/framework6provider/writeonly_move_resource_test.go b/internal/framework6provider/writeonly_move_resource_test.go new file mode 100644 index 0000000..4607e10 --- /dev/null +++ b/internal/framework6provider/writeonly_move_resource_test.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyMoveResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + { + Config: `resource "framework_writeonly_move" "test" { + string_attr = "hello world!" + writeonly_string = "fakepassword" + } + + moved { + from = terraform_data.test + to = framework_writeonly_move.test + }`, + // TODO: Remove this expect error once Framework is updated to null out write-only attributes. + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + // TODO: Remove this additional step once Framework is updated to null out write-only attributes. + // Back to the original config to avoid a destroy clean-up error. + { + Config: `resource "terraform_data" "test" { + input = "hello world!" + }`, + }, + }, + }) +} From f2caa1f60feec65e7957d61e443f5ccdb6e35261 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 Jan 2025 17:44:58 -0500 Subject: [PATCH 33/35] upgrade resource tests --- internal/framework5provider/provider.go | 13 ++- .../writeonly_upgrade_resource.go | 94 +++++++++++++++++++ .../writeonly_upgrade_resource_test.go | 56 +++++++++++ internal/framework6provider/provider.go | 13 ++- .../writeonly_upgrade_resource.go | 94 +++++++++++++++++++ .../writeonly_upgrade_resource_test.go | 56 +++++++++++ 6 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 internal/framework5provider/writeonly_upgrade_resource.go create mode 100644 internal/framework5provider/writeonly_upgrade_resource_test.go create mode 100644 internal/framework6provider/writeonly_upgrade_resource.go create mode 100644 internal/framework6provider/writeonly_upgrade_resource_test.go diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index 928671a..9253513 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -34,8 +34,16 @@ func NewWithEphemeralSpy(spy *EphemeralResourceSpyClient) provider.Provider { } } +func NewWithUpgradeVersion(version int64) provider.Provider { + return &testProvider{ + ephSpyClient: &EphemeralResourceSpyClient{}, + upgradeVersion: version, + } +} + type testProvider struct { - ephSpyClient *EphemeralResourceSpyClient + ephSpyClient *EphemeralResourceSpyClient + upgradeVersion int64 } func (p *testProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { @@ -96,6 +104,9 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewWriteOnlyValidationsResource, NewWriteOnlyImportResource, NewWriteOnlyMoveResource, + func() resource.Resource { + return NewWriteOnlyUpgradeResource(p.upgradeVersion) + }, } } diff --git a/internal/framework5provider/writeonly_upgrade_resource.go b/internal/framework5provider/writeonly_upgrade_resource.go new file mode 100644 index 0000000..af91dbc --- /dev/null +++ b/internal/framework5provider/writeonly_upgrade_resource.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyUpgradeResource{} +var _ resource.ResourceWithUpgradeState = WriteOnlyUpgradeResource{} + +func NewWriteOnlyUpgradeResource(version int64) resource.Resource { + return &WriteOnlyUpgradeResource{ + version: version, + } +} + +type WriteOnlyUpgradeResource struct { + // version is allowing the calling code to determine what the schema version is, to allow us to use a single resource implementation + // and simulate an upgrade. + version int64 +} + +func (r WriteOnlyUpgradeResource) UpgradeState(context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + resp.Diagnostics.Append(resp.State.Set( + ctx, + WriteOnlyUpgradeResourceModel{ + StringAttr: types.StringValue("hello world!"), + WriteOnlyString: types.StringValue("this shouldn't cause an error"), + }, + )...) + }, + }, + } +} + +func (r WriteOnlyUpgradeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_upgrade" +} + +func (r WriteOnlyUpgradeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: r.version, + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyUpgradeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyUpgradeResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyUpgradeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyUpgradeResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyUpgradeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r WriteOnlyUpgradeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnlyUpgradeResourceModel struct { + StringAttr types.String `tfsdk:"string_attr"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` +} diff --git a/internal/framework5provider/writeonly_upgrade_resource_test.go b/internal/framework5provider/writeonly_upgrade_resource_test.go new file mode 100644 index 0000000..62495f1 --- /dev/null +++ b/internal/framework5provider/writeonly_upgrade_resource_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyUpgradeResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(NewWithUpgradeVersion(0)), + }, + Config: `resource "framework_writeonly_upgrade" "test" { + string_attr = "hello!" + writeonly_string = "fakepassword" + }`, + }, + { + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(NewWithUpgradeVersion(1)), + }, + Config: `resource "framework_writeonly_upgrade" "test" { + string_attr = "world!" + writeonly_string = "fakepassword" + }`, + // TODO: Remove this expect error once Framework is updated to null out write-only attributes. + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + // TODO: Remove this additional step once Framework is updated to null out write-only attributes. + // Back to the original config to avoid a destroy clean-up error. + { + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(NewWithUpgradeVersion(0)), + }, + Config: `resource "framework_writeonly_upgrade" "test" { + string_attr = "hello!" + writeonly_string = "fakepassword" + }`, + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index a67fe7e..f08a8c4 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -34,8 +34,16 @@ func NewWithEphemeralSpy(spy *EphemeralResourceSpyClient) provider.Provider { } } +func NewWithUpgradeVersion(version int64) provider.Provider { + return &testProvider{ + ephSpyClient: &EphemeralResourceSpyClient{}, + upgradeVersion: version, + } +} + type testProvider struct { - ephSpyClient *EphemeralResourceSpyClient + ephSpyClient *EphemeralResourceSpyClient + upgradeVersion int64 } func (p *testProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { @@ -96,6 +104,9 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewWriteOnlyValidationsResource, NewWriteOnlyImportResource, NewWriteOnlyMoveResource, + func() resource.Resource { + return NewWriteOnlyUpgradeResource(p.upgradeVersion) + }, } } diff --git a/internal/framework6provider/writeonly_upgrade_resource.go b/internal/framework6provider/writeonly_upgrade_resource.go new file mode 100644 index 0000000..af91dbc --- /dev/null +++ b/internal/framework6provider/writeonly_upgrade_resource.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = WriteOnlyUpgradeResource{} +var _ resource.ResourceWithUpgradeState = WriteOnlyUpgradeResource{} + +func NewWriteOnlyUpgradeResource(version int64) resource.Resource { + return &WriteOnlyUpgradeResource{ + version: version, + } +} + +type WriteOnlyUpgradeResource struct { + // version is allowing the calling code to determine what the schema version is, to allow us to use a single resource implementation + // and simulate an upgrade. + version int64 +} + +func (r WriteOnlyUpgradeResource) UpgradeState(context.Context) map[int64]resource.StateUpgrader { + return map[int64]resource.StateUpgrader{ + 0: { + StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { + resp.Diagnostics.Append(resp.State.Set( + ctx, + WriteOnlyUpgradeResourceModel{ + StringAttr: types.StringValue("hello world!"), + WriteOnlyString: types.StringValue("this shouldn't cause an error"), + }, + )...) + }, + }, + } +} + +func (r WriteOnlyUpgradeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_writeonly_upgrade" +} + +func (r WriteOnlyUpgradeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: r.version, + Attributes: map[string]schema.Attribute{ + "string_attr": schema.StringAttribute{ + Required: true, + }, + "writeonly_string": schema.StringAttribute{ + Optional: true, + WriteOnly: true, + }, + }, + } +} + +func (r WriteOnlyUpgradeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config WriteOnlyUpgradeResourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &config)...) +} + +func (r WriteOnlyUpgradeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data WriteOnlyUpgradeResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r WriteOnlyUpgradeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r WriteOnlyUpgradeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type WriteOnlyUpgradeResourceModel struct { + StringAttr types.String `tfsdk:"string_attr"` + WriteOnlyString types.String `tfsdk:"writeonly_string"` +} diff --git a/internal/framework6provider/writeonly_upgrade_resource_test.go b/internal/framework6provider/writeonly_upgrade_resource_test.go new file mode 100644 index 0000000..ee8c99d --- /dev/null +++ b/internal/framework6provider/writeonly_upgrade_resource_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyUpgradeResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11.0+ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + Steps: []resource.TestStep{ + { + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(NewWithUpgradeVersion(0)), + }, + Config: `resource "framework_writeonly_upgrade" "test" { + string_attr = "hello!" + writeonly_string = "fakepassword" + }`, + }, + { + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(NewWithUpgradeVersion(1)), + }, + Config: `resource "framework_writeonly_upgrade" "test" { + string_attr = "world!" + writeonly_string = "fakepassword" + }`, + // TODO: Remove this expect error once Framework is updated to null out write-only attributes. + ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + }, + // TODO: Remove this additional step once Framework is updated to null out write-only attributes. + // Back to the original config to avoid a destroy clean-up error. + { + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(NewWithUpgradeVersion(0)), + }, + Config: `resource "framework_writeonly_upgrade" "test" { + string_attr = "hello!" + writeonly_string = "fakepassword" + }`, + }, + }, + }) +} From 5111b9983f4a018499d0f234035123b74734b1c9 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 Jan 2025 18:15:49 -0500 Subject: [PATCH 34/35] add upgrade test --- internal/sdkv2provider/provider.go | 8 ++ .../resource_write_only_upgrade.go | 77 +++++++++++++++++++ .../resource_write_only_upgrade_test.go | 58 ++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 internal/sdkv2provider/resource_write_only_upgrade.go create mode 100644 internal/sdkv2provider/resource_write_only_upgrade_test.go diff --git a/internal/sdkv2provider/provider.go b/internal/sdkv2provider/provider.go index 6c60c22..0936ee0 100644 --- a/internal/sdkv2provider/provider.go +++ b/internal/sdkv2provider/provider.go @@ -29,6 +29,7 @@ func New() *schema.Provider { "corner_user": resourceUser(), "corner_writeonly": resourceWriteOnly(), "corner_writeonly_import": resourceWriteOnlyImport(), + "corner_writeonly_upgrade": resourceWriteOnlyUpgrade(0), "corner_writeonce": resourceWriteOnce(), "corner_writeonly_validations": resourceWriteOnlyValidations(), "corner_bigint": resourceBigint(), @@ -54,3 +55,10 @@ func New() *schema.Provider { return p } + +func NewWithUpgradeVersion(version int) *schema.Provider { + p := New() + p.ResourcesMap["corner_writeonly_upgrade"] = resourceWriteOnlyUpgrade(version) + + return p +} diff --git a/internal/sdkv2provider/resource_write_only_upgrade.go b/internal/sdkv2provider/resource_write_only_upgrade.go new file mode 100644 index 0000000..ba718f3 --- /dev/null +++ b/internal/sdkv2provider/resource_write_only_upgrade.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//nolint:forcetypeassert // Test SDK provider +package sdkv2 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceWriteOnlyUpgrade(version int) *schema.Resource { + rSchema := &schema.Resource{ + // Prevent any accidental data inconsistencies + EnableLegacyTypeSystemPlanErrors: true, + EnableLegacyTypeSystemApplyErrors: true, + + SchemaVersion: version, + + CreateContext: resourceWriteOnlyUpgradeCreate, + ReadContext: resourceWriteOnlyUpgradeRead, + UpdateContext: resourceWriteOnlyUpgradeUpdate, + DeleteContext: resourceWriteOnlyUpgradeDelete, + + Schema: map[string]*schema.Schema{ + "string_attr": { + Type: schema.TypeString, + Required: true, + }, + "writeonly_string": { + Type: schema.TypeString, + Optional: true, + WriteOnly: true, + }, + }, + } + + // Avoid the internal validate error for defining a state upgrade that is equal to the schema version + if version > 0 { + rSchema.StateUpgraders = []schema.StateUpgrader{ + { + Version: 0, + Type: rSchema.CoreConfigSchema().ImpliedType(), + Upgrade: func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + upgradedState := map[string]any{ + "id": "fakeid-123", + "string_attr": "hello world!", + "writeonly_string": "this shouldn't cause an error", + } + + return upgradedState, nil + }, + }, + } + } + return rSchema +} + +func resourceWriteOnlyUpgradeCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId("fakeid-123") + + return nil +} + +func resourceWriteOnlyUpgradeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceWriteOnlyUpgradeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} + +func resourceWriteOnlyUpgradeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return nil +} diff --git a/internal/sdkv2provider/resource_write_only_upgrade_test.go b/internal/sdkv2provider/resource_write_only_upgrade_test.go new file mode 100644 index 0000000..b52f933 --- /dev/null +++ b/internal/sdkv2provider/resource_write_only_upgrade_test.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package sdkv2 + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestWriteOnlyUpgradeResource(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Write-only attributes are only available in 1.11 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_11_0), + }, + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "corner": func() (tfprotov5.ProviderServer, error) { //nolint + return NewWithUpgradeVersion(0).GRPCProvider(), nil + }, + }, + Config: `resource "corner_writeonly_upgrade" "test" { + string_attr = "hello!" + writeonly_string = "fakepassword" + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_upgrade.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + }, + }, + { + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "corner": func() (tfprotov5.ProviderServer, error) { //nolint + return NewWithUpgradeVersion(1).GRPCProvider(), nil + }, + }, + Config: `resource "corner_writeonly_upgrade" "test" { + string_attr = "world!" + writeonly_string = "fakepassword" + }`, + // TODO: This test will start erroring when core fixes the state upgrade bug (currently not checking write-only data sent back) + // TODO: This test will then pass once SDKv2 fixes the bug of UpgradeResourceState not nulling out write-only data + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("corner_writeonly_upgrade.test", tfjsonpath.New("writeonly_string"), knownvalue.Null()), + }, + }, + }, + }) +} From 59539e86c25f50316bac0cffa40df1b6a847044e Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 14 Jan 2025 17:19:08 -0500 Subject: [PATCH 35/35] update error messages (will break CI until beta is released) --- .../writeonly_import_resource_test.go | 3 +-- .../writeonly_move_resource_test.go | 2 +- .../writeonly_upgrade_resource_test.go | 2 +- .../writeonly_import_resource_test.go | 3 +-- .../writeonly_move_resource_test.go | 2 +- .../writeonly_upgrade_resource_test.go | 2 +- .../protocolprovider/writeonly_datacheck_test.go | 15 +++++---------- .../writeonly_legacy_datacheck_test.go | 6 ++---- .../writeonly_datacheck_test.go | 15 +++++---------- .../writeonly_legacy_datacheck_test.go | 6 ++---- .../resource_write_only_import_test.go | 2 +- 11 files changed, 21 insertions(+), 37 deletions(-) diff --git a/internal/framework5provider/writeonly_import_resource_test.go b/internal/framework5provider/writeonly_import_resource_test.go index 5bcd055..c4334a5 100644 --- a/internal/framework5provider/writeonly_import_resource_test.go +++ b/internal/framework5provider/writeonly_import_resource_test.go @@ -33,8 +33,7 @@ func TestWriteOnlyImportResource(t *testing.T) { ResourceName: "framework_writeonly_import.test", ImportState: true, ImportStateVerify: true, - // TODO: Remove this expect error once Framework is updated to null out write-only attributes. - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Import returned a non-null value for a write-only attribute`), }, }, }) diff --git a/internal/framework5provider/writeonly_move_resource_test.go b/internal/framework5provider/writeonly_move_resource_test.go index 70f5ff3..f4df110 100644 --- a/internal/framework5provider/writeonly_move_resource_test.go +++ b/internal/framework5provider/writeonly_move_resource_test.go @@ -39,7 +39,7 @@ func TestWriteOnlyMoveResource(t *testing.T) { to = framework_writeonly_move.test }`, // TODO: Remove this expect error once Framework is updated to null out write-only attributes. - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider returned invalid value`), }, // TODO: Remove this additional step once Framework is updated to null out write-only attributes. // Back to the original config to avoid a destroy clean-up error. diff --git a/internal/framework5provider/writeonly_upgrade_resource_test.go b/internal/framework5provider/writeonly_upgrade_resource_test.go index 62495f1..da1f57d 100644 --- a/internal/framework5provider/writeonly_upgrade_resource_test.go +++ b/internal/framework5provider/writeonly_upgrade_resource_test.go @@ -38,7 +38,7 @@ func TestWriteOnlyUpgradeResource(t *testing.T) { writeonly_string = "fakepassword" }`, // TODO: Remove this expect error once Framework is updated to null out write-only attributes. - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, // TODO: Remove this additional step once Framework is updated to null out write-only attributes. // Back to the original config to avoid a destroy clean-up error. diff --git a/internal/framework6provider/writeonly_import_resource_test.go b/internal/framework6provider/writeonly_import_resource_test.go index f4fdf69..80ce7a8 100644 --- a/internal/framework6provider/writeonly_import_resource_test.go +++ b/internal/framework6provider/writeonly_import_resource_test.go @@ -33,8 +33,7 @@ func TestWriteOnlyImportResource(t *testing.T) { ResourceName: "framework_writeonly_import.test", ImportState: true, ImportStateVerify: true, - // TODO: Remove this expect error once Framework is updated to null out write-only attributes. - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Import returned a non-null value for a write-only attribute`), }, }, }) diff --git a/internal/framework6provider/writeonly_move_resource_test.go b/internal/framework6provider/writeonly_move_resource_test.go index 4607e10..5ca383c 100644 --- a/internal/framework6provider/writeonly_move_resource_test.go +++ b/internal/framework6provider/writeonly_move_resource_test.go @@ -39,7 +39,7 @@ func TestWriteOnlyMoveResource(t *testing.T) { to = framework_writeonly_move.test }`, // TODO: Remove this expect error once Framework is updated to null out write-only attributes. - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider returned invalid value`), }, // TODO: Remove this additional step once Framework is updated to null out write-only attributes. // Back to the original config to avoid a destroy clean-up error. diff --git a/internal/framework6provider/writeonly_upgrade_resource_test.go b/internal/framework6provider/writeonly_upgrade_resource_test.go index ee8c99d..27fbec5 100644 --- a/internal/framework6provider/writeonly_upgrade_resource_test.go +++ b/internal/framework6provider/writeonly_upgrade_resource_test.go @@ -38,7 +38,7 @@ func TestWriteOnlyUpgradeResource(t *testing.T) { writeonly_string = "fakepassword" }`, // TODO: Remove this expect error once Framework is updated to null out write-only attributes. - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, // TODO: Remove this additional step once Framework is updated to null out write-only attributes. // Back to the original config to avoid a destroy clean-up error. diff --git a/internal/protocolprovider/writeonly_datacheck_test.go b/internal/protocolprovider/writeonly_datacheck_test.go index 07d66aa..2b845c2 100644 --- a/internal/protocolprovider/writeonly_datacheck_test.go +++ b/internal/protocolprovider/writeonly_datacheck_test.go @@ -69,8 +69,7 @@ func TestAccResourceWriteOnlyDataCheck_plan_error(t *testing.T) { Config: `resource "corner_writeonly_datacheck_planerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid plan`), }, }, }) @@ -93,8 +92,7 @@ func TestAccResourceWriteOnlyDataCheck_apply_error(t *testing.T) { Config: `resource "corner_writeonly_datacheck_applyerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, }, }) @@ -117,8 +115,7 @@ func TestAccResourceWriteOnlyDataCheck_read_error(t *testing.T) { Config: `resource "corner_writeonly_datacheck_readerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, }, }) @@ -155,8 +152,7 @@ func TestAccResourceWriteOnlyDataCheck_import_error(t *testing.T) { ResourceName: "corner_writeonly_datacheck_importerror.test", ImportState: true, ImportStateVerify: true, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Import returned a non-null value for a write-only attribute`), }, }, }) @@ -215,8 +211,7 @@ func TestAccResourceWriteOnlyDataCheck_moveresource_error(t *testing.T) { from = terraform_data.test to = corner_writeonly_datacheck_moveresourceerror.test }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider returned invalid value`), }, // Back to the original config to avoid a destroy clean-up error. { diff --git a/internal/protocolprovider/writeonly_legacy_datacheck_test.go b/internal/protocolprovider/writeonly_legacy_datacheck_test.go index 4878ea6..bcfb363 100644 --- a/internal/protocolprovider/writeonly_legacy_datacheck_test.go +++ b/internal/protocolprovider/writeonly_legacy_datacheck_test.go @@ -69,8 +69,7 @@ func TestAccResourceWriteOnlyLegacyDataCheck_plan_error(t *testing.T) { Config: `resource "corner_writeonly_legacy_datacheck_planerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid plan`), }, }, }) @@ -93,8 +92,7 @@ func TestAccResourceWriteOnlyLegacyDataCheck_apply_error(t *testing.T) { Config: `resource "corner_writeonly_legacy_datacheck_applyerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, }, }) diff --git a/internal/protocolv6provider/writeonly_datacheck_test.go b/internal/protocolv6provider/writeonly_datacheck_test.go index be21422..af3c588 100644 --- a/internal/protocolv6provider/writeonly_datacheck_test.go +++ b/internal/protocolv6provider/writeonly_datacheck_test.go @@ -69,8 +69,7 @@ func TestAccResourceWriteOnlyDataCheck_plan_error(t *testing.T) { Config: `resource "corner_v6_writeonly_datacheck_planerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid plan`), }, }, }) @@ -93,8 +92,7 @@ func TestAccResourceWriteOnlyDataCheck_apply_error(t *testing.T) { Config: `resource "corner_v6_writeonly_datacheck_applyerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, }, }) @@ -117,8 +115,7 @@ func TestAccResourceWriteOnlyDataCheck_read_error(t *testing.T) { Config: `resource "corner_v6_writeonly_datacheck_readerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, }, }) @@ -155,8 +152,7 @@ func TestAccResourceWriteOnlyDataCheck_import_error(t *testing.T) { ResourceName: "corner_v6_writeonly_datacheck_importerror.test", ImportState: true, ImportStateVerify: true, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Import returned a non-null value for a write-only attribute`), }, }, }) @@ -215,8 +211,7 @@ func TestAccResourceWriteOnlyDataCheck_moveresource_error(t *testing.T) { from = terraform_data.test to = corner_v6_writeonly_datacheck_moveresourceerror.test }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider returned invalid value`), }, // Back to the original config to avoid a destroy clean-up error. { diff --git a/internal/protocolv6provider/writeonly_legacy_datacheck_test.go b/internal/protocolv6provider/writeonly_legacy_datacheck_test.go index 0eb96fa..ba67be7 100644 --- a/internal/protocolv6provider/writeonly_legacy_datacheck_test.go +++ b/internal/protocolv6provider/writeonly_legacy_datacheck_test.go @@ -69,8 +69,7 @@ func TestAccResourceWriteOnlyLegacyDataCheck_plan_error(t *testing.T) { Config: `resource "corner_v6_writeonly_legacy_datacheck_planerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid plan`), }, }, }) @@ -93,8 +92,7 @@ func TestAccResourceWriteOnlyLegacyDataCheck_apply_error(t *testing.T) { Config: `resource "corner_v6_writeonly_legacy_datacheck_applyerror" "test" { writeonly_attr = "hello world!" }`, - // TODO: This error message will likely be changed to be more specific before 1.11 GA - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Provider produced invalid object`), }, }, }) diff --git a/internal/sdkv2provider/resource_write_only_import_test.go b/internal/sdkv2provider/resource_write_only_import_test.go index 7f644d8..30127de 100644 --- a/internal/sdkv2provider/resource_write_only_import_test.go +++ b/internal/sdkv2provider/resource_write_only_import_test.go @@ -32,7 +32,7 @@ func TestWriteOnlyImportResource(t *testing.T) { ImportState: true, ImportStateVerify: true, // TODO: Remove this expect error once SDKv2 is updated to null out write-only attributes. - ExpectError: regexp.MustCompile(`Error: Write-only attribute set`), + ExpectError: regexp.MustCompile(`Error: Import returned a non-null value for a write-only attribute`), }, }, })