From 8f559648f9bc38f5c0cc4e7a3028ce43ddc15fca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Nov 2022 19:40:31 +0000 Subject: [PATCH 01/17] Bump github.com/hashicorp/terraform-plugin-framework Bumps [github.com/hashicorp/terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) from 0.16.0 to 0.17.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-framework/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-framework/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-framework/compare/v0.16.0...v0.17.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-framework dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 48d19cc..a3df0f7 100644 --- a/go.mod +++ b/go.mod @@ -4,22 +4,22 @@ go 1.18 require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/terraform-plugin-framework v0.16.0 + github.com/hashicorp/terraform-plugin-framework v0.17.0 ) require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/terraform-plugin-go v0.14.1 // indirect + github.com/hashicorp/terraform-plugin-go v0.14.2 // indirect github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect github.com/vmihailenco/tagparser v0.1.1 // indirect - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect + golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect google.golang.org/appengine v1.6.5 // indirect google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index b7719da..5b2446a 100644 --- a/go.sum +++ b/go.sum @@ -13,10 +13,10 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/terraform-plugin-framework v0.16.0 h1:kEHh0d6dp5Ig/ey6PYXkWDZPMLIW8Me41T/Oa7bpO4s= -github.com/hashicorp/terraform-plugin-framework v0.16.0/go.mod h1:Vk5MuIJoE1qksHZawAZr6psx6YXsQBFIKDrWbROrwus= -github.com/hashicorp/terraform-plugin-go v0.14.1 h1:cwZzPYla82XwAqpLhSzdVsOMU+6H29tczAwrB0z9Zek= -github.com/hashicorp/terraform-plugin-go v0.14.1/go.mod h1:Bc/K6K26BQ2FHqIELPbpKtt2CzzbQou+0UQF3/0NsCQ= +github.com/hashicorp/terraform-plugin-framework v0.17.0 h1:0KUOY/oe1GPLFqaXnKDnd1rhCrnUtt8pV9wGEwNUFlU= +github.com/hashicorp/terraform-plugin-framework v0.17.0/go.mod h1:FV97t2BZOARkL7NNlsc/N25c84MyeSSz72uPp7Vq1lg= +github.com/hashicorp/terraform-plugin-go v0.14.2 h1:rhsVEOGCnY04msNymSvbUsXfRLKh9znXZmHlf5e8mhE= +github.com/hashicorp/terraform-plugin-go v0.14.2/go.mod h1:Q12UjumPNGiFsZffxOsA40Tlz1WVXt2Evh865Zj0+UA= github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs= github.com/hashicorp/terraform-plugin-log v0.7.0/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -32,7 +32,6 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -45,15 +44,16 @@ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 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= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From aa0bcd6dc5f376703ab05ae075b5b23d6d74211c Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 1 Dec 2022 14:12:46 +0000 Subject: [PATCH 02/17] Add new packages for datasourcetimeouts and resourcetimeouts that make use of type-specific schema and attributes --- internal/validators/timeduration.go | 32 ++++ internal/validators/timeduration_test.go | 51 +++++ timeouts/datasource/schema.go | 101 ++++++++++ timeouts/datasource/schema_test.go | 225 +++++++++++++++++++++++ timeouts/resource/schema.go | 101 ++++++++++ timeouts/resource/schema_test.go | 225 +++++++++++++++++++++++ timeouts/schema.go | 17 ++ timeouts/schema_test.go | 4 + 8 files changed, 756 insertions(+) create mode 100644 timeouts/datasource/schema.go create mode 100644 timeouts/datasource/schema_test.go create mode 100644 timeouts/resource/schema.go create mode 100644 timeouts/resource/schema_test.go diff --git a/internal/validators/timeduration.go b/internal/validators/timeduration.go index 9de04bc..23d9a38 100644 --- a/internal/validators/timeduration.go +++ b/internal/validators/timeduration.go @@ -6,11 +6,13 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" ) var _ tfsdk.AttributeValidator = timeDurationValidator{} +var _ validator.String = timeDurationValidator{} // timeDurationValidator validates that a string Attribute's value is parseable as time.Duration. type timeDurationValidator struct { @@ -27,6 +29,8 @@ func (validator timeDurationValidator) MarkdownDescription(ctx context.Context) } // Validate performs the validation. +// +// Deprecated: Use ValidateString instead. func (validator timeDurationValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { s := request.AttributeConfig.(types.String) @@ -44,6 +48,24 @@ func (validator timeDurationValidator) Validate(ctx context.Context, request tfs } } +// ValidateString performs the validation. +func (validator timeDurationValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + s := req.ConfigValue + + if s.IsUnknown() || s.IsNull() { + return + } + + if _, err := time.ParseDuration(s.ValueString()); err != nil { + resp.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + req.Path, + "Invalid Attribute Value Time Duration", + fmt.Sprintf("%q %s", s.ValueString(), validator.Description(ctx))), + ) + return + } +} + // TimeDuration returns an AttributeValidator which ensures that any configured // attribute value: // @@ -53,3 +75,13 @@ func (validator timeDurationValidator) Validate(ctx context.Context, request tfs func TimeDuration() tfsdk.AttributeValidator { return timeDurationValidator{} } + +// TimeDurationString returns an AttributeValidator which ensures that any configured +// attribute value: +// +// - Is parseable as time duration. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func TimeDurationString() validator.String { + return timeDurationValidator{} +} diff --git a/internal/validators/timeduration_test.go b/internal/validators/timeduration_test.go index cb43566..6a81e50 100644 --- a/internal/validators/timeduration_test.go +++ b/internal/validators/timeduration_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -62,3 +63,53 @@ func TestTimeDuration(t *testing.T) { }) } } + +func TestTimeDurationString(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectedDiagnostics diag.Diagnostics + } + + tests := map[string]testCase{ + "unknown": { + val: types.StringUnknown(), + }, + "null": { + val: types.StringNull(), + }, + "valid": { + val: types.StringValue("20m"), + }, + "invalid": { + val: types.StringValue("20x"), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "Invalid Attribute Value Time Duration", + `"20x" must be a string containing a sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`, + ), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + + response := validator.StringResponse{} + + validators.TimeDurationString().ValidateString(context.Background(), request, &response) + + if diff := cmp.Diff(response.Diagnostics, test.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/timeouts/datasource/schema.go b/timeouts/datasource/schema.go new file mode 100644 index 0000000..221879b --- /dev/null +++ b/timeouts/datasource/schema.go @@ -0,0 +1,101 @@ +package datasourcetimeouts + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" +) + +const ( + attributeNameCreate = "create" + attributeNameRead = "read" + attributeNameUpdate = "update" + attributeNameDelete = "delete" +) + +// Opts is used as an argument to Block and Attributes to indicate which attributes +// should be created. +type Opts struct { + Create bool + Read bool + Update bool + Delete bool +} + +// Block returns a schema.Block containing attributes for each of the fields +// in Opts which are set to true. Each attribute is defined as types.StringType +// and optional. A validator is used to verify that the value assigned to an +// attribute can be parsed as time.Duration. +func Block(ctx context.Context, opts Opts) schema.Block { + return schema.SingleNestedBlock{ + Attributes: attributesMap(opts), + } +} + +// BlockAll returns a schema.Block containing attributes for each of create, read, +// update and delete. Each attribute is defined as types.StringType and optional. +// A validator is used to verify that the value assigned to an attribute can be +// parsed as time.Duration. +func BlockAll(ctx context.Context) schema.Block { + return Block(ctx, Opts{ + Create: true, + Read: true, + Update: true, + Delete: true, + }) +} + +// Attributes returns a schema.SingleNestedAttribute +// which contains attributes for each of the fields in Opts which are set to true. +// Each attribute is defined as types.StringType and optional. A validator is used +// to verify that the value assigned to an attribute can be parsed as time.Duration. +func Attributes(ctx context.Context, opts Opts) schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + Attributes: attributesMap(opts), + } +} + +// AttributesAll returns a schema.SingleNestedAttribute +// which contains attributes for each of create, read, update and delete. Each +// attribute is defined as types.StringType and optional. A validator is used to +// verify that the value assigned to an attribute can be parsed as time.Duration. +func AttributesAll(ctx context.Context) schema.Attribute { + return Attributes(ctx, Opts{ + Create: true, + Read: true, + Update: true, + Delete: true, + }) +} + +func attributesMap(opts Opts) map[string]schema.Attribute { + attributes := map[string]schema.Attribute{} + attribute := schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + } + + if opts.Create { + attributes[attributeNameCreate] = attribute + } + + if opts.Read { + attributes[attributeNameRead] = attribute + } + + if opts.Update { + attributes[attributeNameUpdate] = attribute + } + + if opts.Delete { + attributes[attributeNameDelete] = attribute + } + + return attributes +} diff --git a/timeouts/datasource/schema_test.go b/timeouts/datasource/schema_test.go new file mode 100644 index 0000000..d235e4e --- /dev/null +++ b/timeouts/datasource/schema_test.go @@ -0,0 +1,225 @@ +package datasourcetimeouts_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" +) + +func TestBlock(t *testing.T) { + t.Parallel() + + type testCase struct { + opts datasourcetimeouts.Opts + expected schema.Block + } + tests := map[string]testCase{ + "empty-opts": { + opts: datasourcetimeouts.Opts{}, + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{}, + }, + }, + "create-opts": { + opts: datasourcetimeouts.Opts{ + Create: true, + }, + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + "create-update-opts": { + opts: datasourcetimeouts.Opts{ + Create: true, + Update: true, + }, + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := datasourcetimeouts.Block(context.Background(), test.opts) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } + }) + } +} + +func TestBlockAll(t *testing.T) { + t.Parallel() + + actual := datasourcetimeouts.BlockAll(context.Background()) + + expected := schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "delete": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + } + + if diff := cmp.Diff(actual, expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } +} + +func TestAttributes(t *testing.T) { + t.Parallel() + + type testCase struct { + opts datasourcetimeouts.Opts + expected schema.Attribute + } + tests := map[string]testCase{ + "empty-opts": { + opts: datasourcetimeouts.Opts{}, + expected: schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{}, + }, + }, + "create-opts": { + opts: datasourcetimeouts.Opts{ + Create: true, + }, + expected: schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + "create-update-opts": { + opts: datasourcetimeouts.Opts{ + Create: true, + Update: true, + }, + expected: schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := datasourcetimeouts.Attributes(context.Background(), test.opts) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } + }) + } +} + +func TestAttributesAll(t *testing.T) { + t.Parallel() + + actual := datasourcetimeouts.AttributesAll(context.Background()) + + expected := schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "delete": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + } + + if diff := cmp.Diff(actual, expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } +} diff --git a/timeouts/resource/schema.go b/timeouts/resource/schema.go new file mode 100644 index 0000000..a1fe968 --- /dev/null +++ b/timeouts/resource/schema.go @@ -0,0 +1,101 @@ +package resourcetimeouts + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" +) + +const ( + attributeNameCreate = "create" + attributeNameRead = "read" + attributeNameUpdate = "update" + attributeNameDelete = "delete" +) + +// Opts is used as an argument to Block and Attributes to indicate which attributes +// should be created. +type Opts struct { + Create bool + Read bool + Update bool + Delete bool +} + +// Block returns a schema.Block containing attributes for each of the fields +// in Opts which are set to true. Each attribute is defined as types.StringType +// and optional. A validator is used to verify that the value assigned to an +// attribute can be parsed as time.Duration. +func Block(ctx context.Context, opts Opts) schema.Block { + return schema.SingleNestedBlock{ + Attributes: attributesMap(opts), + } +} + +// BlockAll returns a schema.Block containing attributes for each of create, read, +// update and delete. Each attribute is defined as types.StringType and optional. +// A validator is used to verify that the value assigned to an attribute can be +// parsed as time.Duration. +func BlockAll(ctx context.Context) schema.Block { + return Block(ctx, Opts{ + Create: true, + Read: true, + Update: true, + Delete: true, + }) +} + +// Attributes returns a schema.SingleNestedAttribute which contains attributes for +// each of the fields in Opts which are set to true. Each attribute is defined as +// types.StringType and optional. A validator is used to verify that the value +// assigned to an attribute can be parsed as time.Duration. +func Attributes(ctx context.Context, opts Opts) schema.Attribute { + return schema.SingleNestedAttribute{ + Optional: true, + Attributes: attributesMap(opts), + } +} + +// AttributesAll returns a schema.SingleNestedAttribute which contains attributes +// for each of create, read, update and delete. Each attribute is defined as +// types.StringType and optional. A validator is used to verify that the value +// assigned to an attribute can be parsed as time.Duration. +func AttributesAll(ctx context.Context) schema.Attribute { + return Attributes(ctx, Opts{ + Create: true, + Read: true, + Update: true, + Delete: true, + }) +} + +func attributesMap(opts Opts) map[string]schema.Attribute { + attributes := map[string]schema.Attribute{} + attribute := schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + } + + if opts.Create { + attributes[attributeNameCreate] = attribute + } + + if opts.Read { + attributes[attributeNameRead] = attribute + } + + if opts.Update { + attributes[attributeNameUpdate] = attribute + } + + if opts.Delete { + attributes[attributeNameDelete] = attribute + } + + return attributes +} diff --git a/timeouts/resource/schema_test.go b/timeouts/resource/schema_test.go new file mode 100644 index 0000000..2500439 --- /dev/null +++ b/timeouts/resource/schema_test.go @@ -0,0 +1,225 @@ +package resourcetimeouts_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" +) + +func TestBlock(t *testing.T) { + t.Parallel() + + type testCase struct { + opts resourcetimeouts.Opts + expected schema.Block + } + tests := map[string]testCase{ + "empty-opts": { + opts: resourcetimeouts.Opts{}, + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{}, + }, + }, + "create-opts": { + opts: resourcetimeouts.Opts{ + Create: true, + }, + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + "create-update-opts": { + opts: resourcetimeouts.Opts{ + Create: true, + Update: true, + }, + expected: schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := resourcetimeouts.Block(context.Background(), test.opts) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } + }) + } +} + +func TestBlockAll(t *testing.T) { + t.Parallel() + + actual := resourcetimeouts.BlockAll(context.Background()) + + expected := schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "delete": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + } + + if diff := cmp.Diff(actual, expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } +} + +func TestAttributes(t *testing.T) { + t.Parallel() + + type testCase struct { + opts resourcetimeouts.Opts + expected schema.Attribute + } + tests := map[string]testCase{ + "empty-opts": { + opts: resourcetimeouts.Opts{}, + expected: schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{}, + }, + }, + "create-opts": { + opts: resourcetimeouts.Opts{ + Create: true, + }, + expected: schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + "create-update-opts": { + opts: resourcetimeouts.Opts{ + Create: true, + Update: true, + }, + expected: schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := resourcetimeouts.Attributes(context.Background(), test.opts) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } + }) + } +} + +func TestAttributesAll(t *testing.T) { + t.Parallel() + + actual := resourcetimeouts.AttributesAll(context.Background()) + + expected := schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + "delete": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, + }, + }, + } + + if diff := cmp.Diff(actual, expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } +} diff --git a/timeouts/schema.go b/timeouts/schema.go index bfccf25..46446cb 100644 --- a/timeouts/schema.go +++ b/timeouts/schema.go @@ -29,6 +29,10 @@ type Opts struct { // in Opts which are set to true. Each attribute is defined as types.StringType // and optional. A validator is used to verify that the value assigned to an // attribute can be parsed as time.Duration. +// +// Deprecated: Use resourcetimeouts.Block or datasourcetimeouts.Block instead. +// +//nolint:staticcheck func Block(ctx context.Context, opts Opts) tfsdk.Block { return tfsdk.Block{ Attributes: attributesMap(opts), @@ -40,6 +44,10 @@ func Block(ctx context.Context, opts Opts) tfsdk.Block { // update and delete. Each attribute is defined as types.StringType and optional. // A validator is used to verify that the value assigned to an attribute can be // parsed as time.Duration. +// +// Deprecated: Use resourcetimeouts.BlockAll or datasourcetimeouts.BlockAll instead. +// +//nolint:staticcheck func BlockAll(ctx context.Context) tfsdk.Block { return Block(ctx, Opts{ Create: true, @@ -53,6 +61,10 @@ func BlockAll(ctx context.Context) tfsdk.Block { // which contains attributes for each of the fields in Opts which are set to true. // Each attribute is defined as types.StringType and optional. A validator is used // to verify that the value assigned to an attribute can be parsed as time.Duration. +// +// Deprecated: Use resourcetimeouts.Attributes or datasourcetimeouts.Attributes instead. +// +//nolint:staticcheck func Attributes(ctx context.Context, opts Opts) tfsdk.Attribute { return tfsdk.Attribute{ Optional: true, @@ -64,6 +76,10 @@ func Attributes(ctx context.Context, opts Opts) tfsdk.Attribute { // which contains attributes for each of create, read, update and delete. Each // attribute is defined as types.StringType and optional. A validator is used to // verify that the value assigned to an attribute can be parsed as time.Duration. +// +// Deprecated: Use resourcetimeouts.AttributesAll or datasourcetimeouts.AttributesAll instead. +// +//nolint:staticcheck func AttributesAll(ctx context.Context) tfsdk.Attribute { return Attributes(ctx, Opts{ Create: true, @@ -73,6 +89,7 @@ func AttributesAll(ctx context.Context) tfsdk.Attribute { }) } +//nolint:staticcheck func attributesMap(opts Opts) map[string]tfsdk.Attribute { attributes := map[string]tfsdk.Attribute{} attribute := tfsdk.Attribute{ diff --git a/timeouts/schema_test.go b/timeouts/schema_test.go index af10dc5..1f1c514 100644 --- a/timeouts/schema_test.go +++ b/timeouts/schema_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" ) +//nolint:staticcheck func TestBlock(t *testing.T) { t.Parallel() @@ -83,6 +84,7 @@ func TestBlock(t *testing.T) { } } +//nolint:staticcheck func TestBlockAll(t *testing.T) { t.Parallel() @@ -127,6 +129,7 @@ func TestBlockAll(t *testing.T) { } } +//nolint:staticcheck func TestAttributes(t *testing.T) { t.Parallel() @@ -198,6 +201,7 @@ func TestAttributes(t *testing.T) { } } +//nolint:staticcheck func TestAttributesAll(t *testing.T) { t.Parallel() From 60c7d37961be16d178c98e6ab50e517d14fed867 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 7 Dec 2022 11:51:34 +0000 Subject: [PATCH 03/17] Add custom type and value for timeouts. Add resource and datasource schema-specific functions to add a nested blocks or nested attributes into a schema (#17) --- timeouts/datasource/schema.go | 39 ++- timeouts/datasource/schema_test.go | 75 +++++- timeouts/resource/schema.go | 39 ++- timeouts/resource/schema_test.go | 75 +++++- timeouts/type/timeouts.go | 111 ++++++++ timeouts/type/timeouts_test.go | 417 +++++++++++++++++++++++++++++ 6 files changed, 740 insertions(+), 16 deletions(-) create mode 100644 timeouts/type/timeouts.go create mode 100644 timeouts/type/timeouts_test.go diff --git a/timeouts/datasource/schema.go b/timeouts/datasource/schema.go index 221879b..59ed8b9 100644 --- a/timeouts/datasource/schema.go +++ b/timeouts/datasource/schema.go @@ -1,12 +1,15 @@ -package datasourcetimeouts +package timeouts import ( "context" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" + timeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" ) const ( @@ -32,6 +35,11 @@ type Opts struct { func Block(ctx context.Context, opts Opts) schema.Block { return schema.SingleNestedBlock{ Attributes: attributesMap(opts), + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(opts), + }, + }, } } @@ -54,8 +62,13 @@ func BlockAll(ctx context.Context) schema.Block { // to verify that the value assigned to an attribute can be parsed as time.Duration. func Attributes(ctx context.Context, opts Opts) schema.Attribute { return schema.SingleNestedAttribute{ - Optional: true, Attributes: attributesMap(opts), + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(opts), + }, + }, + Optional: true, } } @@ -99,3 +112,25 @@ func attributesMap(opts Opts) map[string]schema.Attribute { return attributes } + +func attrTypesMap(opts Opts) map[string]attr.Type { + attrTypes := map[string]attr.Type{} + + if opts.Create { + attrTypes[attributeNameCreate] = types.StringType + } + + if opts.Read { + attrTypes[attributeNameRead] = types.StringType + } + + if opts.Update { + attrTypes[attributeNameUpdate] = types.StringType + } + + if opts.Delete { + attrTypes[attributeNameDelete] = types.StringType + } + + return attrTypes +} diff --git a/timeouts/datasource/schema_test.go b/timeouts/datasource/schema_test.go index d235e4e..988b1fa 100644 --- a/timeouts/datasource/schema_test.go +++ b/timeouts/datasource/schema_test.go @@ -1,15 +1,18 @@ -package datasourcetimeouts_test +package timeouts_test import ( "context" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" + datasourcetimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" ) func TestBlock(t *testing.T) { @@ -23,6 +26,11 @@ func TestBlock(t *testing.T) { "empty-opts": { opts: datasourcetimeouts.Opts{}, expected: schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{}, + }, + }, Attributes: map[string]schema.Attribute{}, }, }, @@ -31,6 +39,13 @@ func TestBlock(t *testing.T) { Create: true, }, expected: schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + }, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -47,6 +62,14 @@ func TestBlock(t *testing.T) { Update: true, }, expected: schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "update": types.StringType, + }, + }, + }, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -83,6 +106,16 @@ func TestBlockAll(t *testing.T) { actual := datasourcetimeouts.BlockAll(context.Background()) expected := schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "read": types.StringType, + "update": types.StringType, + "delete": types.StringType, + }, + }, + }, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -127,8 +160,13 @@ func TestAttributes(t *testing.T) { "empty-opts": { opts: datasourcetimeouts.Opts{}, expected: schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{}, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{}, + }, + }, + Optional: true, }, }, "create-opts": { @@ -136,7 +174,6 @@ func TestAttributes(t *testing.T) { Create: true, }, expected: schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -145,6 +182,14 @@ func TestAttributes(t *testing.T) { }, }, }, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + }, + Optional: true, }, }, "create-update-opts": { @@ -153,7 +198,6 @@ func TestAttributes(t *testing.T) { Update: true, }, expected: schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -168,6 +212,15 @@ func TestAttributes(t *testing.T) { }, }, }, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "update": types.StringType, + }, + }, + }, + Optional: true, }, }, } @@ -190,7 +243,6 @@ func TestAttributesAll(t *testing.T) { actual := datasourcetimeouts.AttributesAll(context.Background()) expected := schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -217,6 +269,17 @@ func TestAttributesAll(t *testing.T) { }, }, }, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "read": types.StringType, + "update": types.StringType, + "delete": types.StringType, + }, + }, + }, + Optional: true, } if diff := cmp.Diff(actual, expected); diff != "" { diff --git a/timeouts/resource/schema.go b/timeouts/resource/schema.go index a1fe968..482e6a4 100644 --- a/timeouts/resource/schema.go +++ b/timeouts/resource/schema.go @@ -1,12 +1,15 @@ -package resourcetimeouts +package timeouts import ( "context" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" ) const ( @@ -32,6 +35,11 @@ type Opts struct { func Block(ctx context.Context, opts Opts) schema.Block { return schema.SingleNestedBlock{ Attributes: attributesMap(opts), + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(opts), + }, + }, } } @@ -54,8 +62,13 @@ func BlockAll(ctx context.Context) schema.Block { // assigned to an attribute can be parsed as time.Duration. func Attributes(ctx context.Context, opts Opts) schema.Attribute { return schema.SingleNestedAttribute{ - Optional: true, Attributes: attributesMap(opts), + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(opts), + }, + }, + Optional: true, } } @@ -99,3 +112,25 @@ func attributesMap(opts Opts) map[string]schema.Attribute { return attributes } + +func attrTypesMap(opts Opts) map[string]attr.Type { + attrTypes := map[string]attr.Type{} + + if opts.Create { + attrTypes[attributeNameCreate] = types.StringType + } + + if opts.Read { + attrTypes[attributeNameRead] = types.StringType + } + + if opts.Update { + attrTypes[attributeNameUpdate] = types.StringType + } + + if opts.Delete { + attrTypes[attributeNameDelete] = types.StringType + } + + return attrTypes +} diff --git a/timeouts/resource/schema_test.go b/timeouts/resource/schema_test.go index 2500439..e8a4da8 100644 --- a/timeouts/resource/schema_test.go +++ b/timeouts/resource/schema_test.go @@ -1,15 +1,18 @@ -package resourcetimeouts_test +package timeouts_test import ( "context" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + resourcetimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" ) func TestBlock(t *testing.T) { @@ -23,6 +26,11 @@ func TestBlock(t *testing.T) { "empty-opts": { opts: resourcetimeouts.Opts{}, expected: schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{}, + }, + }, Attributes: map[string]schema.Attribute{}, }, }, @@ -31,6 +39,13 @@ func TestBlock(t *testing.T) { Create: true, }, expected: schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + }, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -47,6 +62,14 @@ func TestBlock(t *testing.T) { Update: true, }, expected: schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "update": types.StringType, + }, + }, + }, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -83,6 +106,16 @@ func TestBlockAll(t *testing.T) { actual := resourcetimeouts.BlockAll(context.Background()) expected := schema.SingleNestedBlock{ + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "read": types.StringType, + "update": types.StringType, + "delete": types.StringType, + }, + }, + }, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -127,8 +160,13 @@ func TestAttributes(t *testing.T) { "empty-opts": { opts: resourcetimeouts.Opts{}, expected: schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{}, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{}, + }, + }, + Optional: true, }, }, "create-opts": { @@ -136,7 +174,6 @@ func TestAttributes(t *testing.T) { Create: true, }, expected: schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -145,6 +182,14 @@ func TestAttributes(t *testing.T) { }, }, }, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + }, + Optional: true, }, }, "create-update-opts": { @@ -153,7 +198,6 @@ func TestAttributes(t *testing.T) { Update: true, }, expected: schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -168,6 +212,15 @@ func TestAttributes(t *testing.T) { }, }, }, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "update": types.StringType, + }, + }, + }, + Optional: true, }, }, } @@ -190,7 +243,6 @@ func TestAttributesAll(t *testing.T) { actual := resourcetimeouts.AttributesAll(context.Background()) expected := schema.SingleNestedAttribute{ - Optional: true, Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, @@ -217,6 +269,17 @@ func TestAttributesAll(t *testing.T) { }, }, }, + CustomType: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "read": types.StringType, + "update": types.StringType, + "delete": types.StringType, + }, + }, + }, + Optional: true, } if diff := cmp.Diff(actual, expected); diff != "" { diff --git a/timeouts/type/timeouts.go b/timeouts/type/timeouts.go new file mode 100644 index 0000000..211b069 --- /dev/null +++ b/timeouts/type/timeouts.go @@ -0,0 +1,111 @@ +package timeouts + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +const ( + defaultTimeout = time.Minute * 20 + attributeNameCreate = "create" + attributeNameRead = "read" + attributeNameUpdate = "update" + attributeNameDelete = "delete" +) + +// TimeoutsType is an attribute type that represents timeouts. +type TimeoutsType struct { + types.ObjectType +} + +// ValueFromTerraform returns a TimeoutsValue given a tftypes.Value. +// TimeoutsValue embeds the types.Object value returned from calling ValueFromTerraform on the +// types.ObjectType embedded in TimeoutsType. +func (t TimeoutsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + val, err := t.ObjectType.ValueFromTerraform(ctx, in) + if err != nil { + return nil, err + } + + obj, ok := val.(types.Object) + if !ok { + return nil, fmt.Errorf("%T cannot be used as types.Object", val) + } + + return TimeoutsValue{ + obj, + }, err +} + +// Equal returns true if `candidate` is also an TimeoutsType and has the same +// AttributeTypes. +func (t TimeoutsType) Equal(candidate attr.Type) bool { + other, ok := candidate.(TimeoutsType) + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +// TimeoutsValue represents an object containing values to be used as time.Duration for timeouts. +type TimeoutsValue struct { + types.Object +} + +// Equal returns true if the TimeoutsValue is considered semantically equal +// (same type and same value) to the attr.Value passed as an argument. +func (t TimeoutsValue) Equal(c attr.Value) bool { + other, ok := c.(TimeoutsValue) + + if !ok { + return false + } + + return t.Object.Equal(other.Object) +} + +// Create attempts to retrieve the "create" attribute and parse it as time.Duration. +// If any errors are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Create(ctx context.Context) (time.Duration, error) { + return t.getTimeout(ctx, attributeNameCreate) +} + +// Read attempts to retrieve the "read" attribute and parse it as time.Duration. +// If any errors are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Read(ctx context.Context) (time.Duration, error) { + return t.getTimeout(ctx, attributeNameRead) +} + +// Update attempts to retrieve the "update" attribute and parse it as time.Duration. +// If any errors are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Update(ctx context.Context) (time.Duration, error) { + return t.getTimeout(ctx, attributeNameUpdate) +} + +// Delete attempts to retrieve the "delete" attribute and parse it as time.Duration. +// If any errors are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Delete(ctx context.Context) (time.Duration, error) { + return t.getTimeout(ctx, attributeNameDelete) +} + +func (t TimeoutsValue) getTimeout(ctx context.Context, timeoutName string) (time.Duration, error) { + value, ok := t.Object.Attributes()[timeoutName] + if !ok { + return defaultTimeout, fmt.Errorf("timeout for %q does not exist", timeoutName) + } + + // No type assertion check is required as the schema guarantees that the object attributes + // are types.String. + createTimeout, err := time.ParseDuration(value.(types.String).ValueString()) + if err != nil { + return defaultTimeout, err + } + + return createTimeout, nil +} diff --git a/timeouts/type/timeouts_test.go b/timeouts/type/timeouts_test.go new file mode 100644 index 0000000..37f7c1a --- /dev/null +++ b/timeouts/type/timeouts_test.go @@ -0,0 +1,417 @@ +package timeouts_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" +) + +func TestTimeoutsTypeValueFromTerraform(t *testing.T) { + t.Parallel() + + type testCase struct { + receiver timeouts.TimeoutsType + input tftypes.Value + expected attr.Value + expectedErr string + } + tests := map[string]testCase{ + "basic-object": { + receiver: timeouts.TimeoutsType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "read": types.StringType, + "update": types.StringType, + "delete": types.StringType, + }, + }, + }, + input: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "create": tftypes.String, + "read": tftypes.String, + "update": tftypes.String, + "delete": tftypes.String, + }, + }, map[string]tftypes.Value{ + "create": tftypes.NewValue(tftypes.String, "60m"), + "read": tftypes.NewValue(tftypes.String, "30m"), + "update": tftypes.NewValue(tftypes.String, "10m"), + "delete": tftypes.NewValue(tftypes.String, "25m"), + }), + expected: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "create": types.StringType, + "read": types.StringType, + "update": types.StringType, + "delete": types.StringType, + }, + map[string]attr.Value{ + "create": types.StringValue("60m"), + "read": types.StringValue("30m"), + "update": types.StringValue("10m"), + "delete": types.StringValue("25m"), + }, + ), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := test.receiver.ValueFromTerraform(context.Background(), test.input) + if err != nil { + if test.expectedErr == "" { + t.Errorf("Unexpected error: %s", err.Error()) + return + } + if err.Error() != test.expectedErr { + t.Errorf("Expected error to be %q, got %q", test.expectedErr, err.Error()) + return + } + } + + if diff := cmp.Diff(test.expected, got); diff != "" { + t.Errorf("unexpected result (-expected, +got): %s", diff) + } + }) + } +} + +func TestTimeoutsTypeEqual(t *testing.T) { + t.Parallel() + + type testCase struct { + receiver timeouts.TimeoutsType + input attr.Type + expected bool + } + tests := map[string]testCase{ + "equal": { + receiver: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "c": types.BoolType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + input: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "c": types.BoolType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + expected: true, + }, + "missing-attr": { + receiver: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "c": types.BoolType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + input: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + expected: false, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.receiver.Equal(test.input) + if test.expected != got { + t.Errorf("Expected %v, got %v", test.expected, got) + } + }) + } +} + +func TestTimeoutsValueCreate(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.TimeoutsValue + expectedTimeout time.Duration + expectedErr error + } + tests := map[string]testCase{ + "create": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "create": types.StringType, + }, + map[string]attr.Value{ + "create": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedErr: nil, + }, + "create-not-set": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`timeout for "create" does not exist`), + }, + "create-not-parseable-as-time-duration": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "create": types.StringType, + }, + map[string]attr.Value{ + "create": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotTimeout, gotErr := test.timeoutsValue.Create(context.Background()) + + if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { + t.Errorf("unexpected timeout difference: %s", diff) + } + + if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +func TestTimeoutsValueRead(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.TimeoutsValue + expectedTimeout time.Duration + expectedErr error + } + tests := map[string]testCase{ + "read": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "read": types.StringType, + }, + map[string]attr.Value{ + "read": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedErr: nil, + }, + "read-not-set": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`timeout for "read" does not exist`), + }, + "read-not-parseable-as-time-duration": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "read": types.StringType, + }, + map[string]attr.Value{ + "read": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotTimeout, gotErr := test.timeoutsValue.Read(context.Background()) + + if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { + t.Errorf("unexpected timeout difference: %s", diff) + } + + if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +func TestTimeoutsValueUpdate(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.TimeoutsValue + expectedTimeout time.Duration + expectedErr error + } + tests := map[string]testCase{ + "update": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "update": types.StringType, + }, + map[string]attr.Value{ + "update": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedErr: nil, + }, + "update-not-set": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`timeout for "update" does not exist`), + }, + "update-not-parseable-as-time-duration": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "update": types.StringType, + }, + map[string]attr.Value{ + "update": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotTimeout, gotErr := test.timeoutsValue.Update(context.Background()) + + if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { + t.Errorf("unexpected timeout difference: %s", diff) + } + + if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +func TestTimeoutsValueDelete(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.TimeoutsValue + expectedTimeout time.Duration + expectedErr error + } + tests := map[string]testCase{ + "delete": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "delete": types.StringType, + }, + map[string]attr.Value{ + "delete": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedErr: nil, + }, + "delete-not-set": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`timeout for "delete" does not exist`), + }, + "delete-not-parseable-as-time-duration": { + timeoutsValue: timeouts.TimeoutsValue{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "delete": types.StringType, + }, + map[string]attr.Value{ + "delete": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotTimeout, gotErr := test.timeoutsValue.Delete(context.Background()) + + if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { + t.Errorf("unexpected timeout difference: %s", diff) + } + + if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +// equateErrorMessage reports errors to be equal if both are nil +// or both have the same message. +var equateErrorMessage = cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.Error() == y.Error() +}) From 810210d3578698ee35c03ec6654b862dba904aca Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 7 Dec 2022 12:27:31 +0000 Subject: [PATCH 04/17] Adding Type function to TimeoutsValue (#17) --- timeouts/type/timeouts.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/timeouts/type/timeouts.go b/timeouts/type/timeouts.go index 211b069..0d13922 100644 --- a/timeouts/type/timeouts.go +++ b/timeouts/type/timeouts.go @@ -70,6 +70,15 @@ func (t TimeoutsValue) Equal(c attr.Value) bool { return t.Object.Equal(other.Object) } +// Type returns an TimeoutsType with the same attribute types as `t`. +func (t TimeoutsValue) Type(ctx context.Context) attr.Type { + return TimeoutsType{ + types.ObjectType{ + AttrTypes: t.AttributeTypes(ctx), + }, + } +} + // Create attempts to retrieve the "create" attribute and parse it as time.Duration. // If any errors are generated they are returned along with the default timeout of 20 minutes. func (t TimeoutsValue) Create(ctx context.Context) (time.Duration, error) { From 5bc9230df491ae5e27f663b42c60884b8da682ac Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 7 Dec 2022 12:31:49 +0000 Subject: [PATCH 05/17] Adding changelog entry (#17) --- .changelog/18.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changelog/18.txt diff --git a/.changelog/18.txt b/.changelog/18.txt new file mode 100644 index 0000000..5696e44 --- /dev/null +++ b/.changelog/18.txt @@ -0,0 +1,11 @@ +```release-note:feature +Introduced datasource `timeouts` package for use with datasource schema +``` + +```release-note:feature +Introduced resource `timeouts` package for use with resource schema +``` + +```release-note:feature +Introduced type `timeouts` package to provider `TimeoutsType` and `TimeoutsValue` for use with schema and data models, respectively +``` From f9ecfa25e7311dfd7bbcc01e690269a6e9052d62 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 7 Dec 2022 12:39:51 +0000 Subject: [PATCH 06/17] Update README (#17) --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 59e9ad5..564e967 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ resource "timeouts_example" "example" { } ``` -You can use this module to mutate the `tfsdk.Schema` as follows: +You can use this module to mutate the `schema.Schema` as follows: ```go -func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { return tfsdk.Schema{ /* ... */ @@ -76,10 +76,10 @@ resource "timeouts_example" "example" { } ``` -You can use this module to mutate the `tfsdk.Schema` as follows: +You can use this module to mutate the `schema.Schema` as follows: ```go -func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { return tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ /* ... */ @@ -102,12 +102,12 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, ``` The model that is being used, `exampleResourceData` in this example, will need to be modified to include a field for -timeouts which is of `types.Object`. For example: +timeouts which is of type `timeouts.TimeoutsValue`. For example: ```go type exampleResourceData struct { /* ... */ - Timeouts types.Object `tfsdk:"timeouts"` + Timeouts timeouts.TimeoutsValue `tfsdk:"timeouts"` ``` ### Accessing Timeouts in CRUD Functions @@ -125,13 +125,14 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, return } - defaultCreateTimeout := 20 * time.Minutes + createTimeout, err := data.Timeouts.Create(ctx) + if err != nil { + // handle error + } - createTimeout := timeouts.Create(ctx, data.Timeouts, defaultCreateTimeout) - ctx, cancel := context.WithTimeout(ctx, createTimeout) defer cancel() - + /* ... */ } ``` From 943ba4c6744b97da62bc2c9c598b11a16a1df4ce Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 7 Dec 2022 12:43:22 +0000 Subject: [PATCH 07/17] Fix linting (#17) --- timeouts/type/timeouts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timeouts/type/timeouts.go b/timeouts/type/timeouts.go index 0d13922..2312c38 100644 --- a/timeouts/type/timeouts.go +++ b/timeouts/type/timeouts.go @@ -103,7 +103,7 @@ func (t TimeoutsValue) Delete(ctx context.Context) (time.Duration, error) { return t.getTimeout(ctx, attributeNameDelete) } -func (t TimeoutsValue) getTimeout(ctx context.Context, timeoutName string) (time.Duration, error) { +func (t TimeoutsValue) getTimeout(_ context.Context, timeoutName string) (time.Duration, error) { value, ok := t.Object.Attributes()[timeoutName] if !ok { return defaultTimeout, fmt.Errorf("timeout for %q does not exist", timeoutName) From 97bff1599216607f879578b43e75e382838763f1 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 13 Dec 2022 12:33:12 +0000 Subject: [PATCH 08/17] Making Read the attribute for timeouts on data sources (#17) --- timeouts/datasource/schema.go | 116 ++++------------ timeouts/datasource/schema_test.go | 207 ++--------------------------- 2 files changed, 33 insertions(+), 290 deletions(-) diff --git a/timeouts/datasource/schema.go b/timeouts/datasource/schema.go index 59ed8b9..9dfb796 100644 --- a/timeouts/datasource/schema.go +++ b/timeouts/datasource/schema.go @@ -13,124 +13,58 @@ import ( ) const ( - attributeNameCreate = "create" - attributeNameRead = "read" - attributeNameUpdate = "update" - attributeNameDelete = "delete" + attributeNameRead = "read" ) // Opts is used as an argument to Block and Attributes to indicate which attributes // should be created. type Opts struct { - Create bool - Read bool - Update bool - Delete bool + Read bool } -// Block returns a schema.Block containing attributes for each of the fields -// in Opts which are set to true. Each attribute is defined as types.StringType -// and optional. A validator is used to verify that the value assigned to an -// attribute can be parsed as time.Duration. -func Block(ctx context.Context, opts Opts) schema.Block { +// Block returns a schema.Block containing attributes for `Read`, which is +// defined as types.StringType and optional. A validator is used to verify +// that the value assigned to `Read` can be parsed as time.Duration. +func Block(ctx context.Context) schema.Block { return schema.SingleNestedBlock{ - Attributes: attributesMap(opts), + Attributes: attributesMap(), CustomType: timeouts.TimeoutsType{ ObjectType: types.ObjectType{ - AttrTypes: attrTypesMap(opts), + AttrTypes: attrTypesMap(), }, }, } } -// BlockAll returns a schema.Block containing attributes for each of create, read, -// update and delete. Each attribute is defined as types.StringType and optional. -// A validator is used to verify that the value assigned to an attribute can be -// parsed as time.Duration. -func BlockAll(ctx context.Context) schema.Block { - return Block(ctx, Opts{ - Create: true, - Read: true, - Update: true, - Delete: true, - }) -} - -// Attributes returns a schema.SingleNestedAttribute -// which contains attributes for each of the fields in Opts which are set to true. -// Each attribute is defined as types.StringType and optional. A validator is used -// to verify that the value assigned to an attribute can be parsed as time.Duration. -func Attributes(ctx context.Context, opts Opts) schema.Attribute { +// Attributes returns a schema.SingleNestedAttribute which contains an +// attribute for `Read`, which is defined as types.StringType and optional. +// A validator is used to verify that the value assigned to an attribute +// can be parsed as time.Duration. +func Attributes(ctx context.Context) schema.Attribute { return schema.SingleNestedAttribute{ - Attributes: attributesMap(opts), + Attributes: attributesMap(), CustomType: timeouts.TimeoutsType{ ObjectType: types.ObjectType{ - AttrTypes: attrTypesMap(opts), + AttrTypes: attrTypesMap(), }, }, Optional: true, } } -// AttributesAll returns a schema.SingleNestedAttribute -// which contains attributes for each of create, read, update and delete. Each -// attribute is defined as types.StringType and optional. A validator is used to -// verify that the value assigned to an attribute can be parsed as time.Duration. -func AttributesAll(ctx context.Context) schema.Attribute { - return Attributes(ctx, Opts{ - Create: true, - Read: true, - Update: true, - Delete: true, - }) -} - -func attributesMap(opts Opts) map[string]schema.Attribute { - attributes := map[string]schema.Attribute{} - attribute := schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), +func attributesMap() map[string]schema.Attribute { + return map[string]schema.Attribute{ + attributeNameRead: schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDurationString(), + }, }, } - - if opts.Create { - attributes[attributeNameCreate] = attribute - } - - if opts.Read { - attributes[attributeNameRead] = attribute - } - - if opts.Update { - attributes[attributeNameUpdate] = attribute - } - - if opts.Delete { - attributes[attributeNameDelete] = attribute - } - - return attributes } -func attrTypesMap(opts Opts) map[string]attr.Type { - attrTypes := map[string]attr.Type{} - - if opts.Create { - attrTypes[attributeNameCreate] = types.StringType - } - - if opts.Read { - attrTypes[attributeNameRead] = types.StringType +func attrTypesMap() map[string]attr.Type { + return map[string]attr.Type{ + attributeNameRead: types.StringType, } - - if opts.Update { - attrTypes[attributeNameUpdate] = types.StringType - } - - if opts.Delete { - attrTypes[attributeNameDelete] = types.StringType - } - - return attrTypes } diff --git a/timeouts/datasource/schema_test.go b/timeouts/datasource/schema_test.go index 988b1fa..277b5f8 100644 --- a/timeouts/datasource/schema_test.go +++ b/timeouts/datasource/schema_test.go @@ -19,65 +19,20 @@ func TestBlock(t *testing.T) { t.Parallel() type testCase struct { - opts datasourcetimeouts.Opts expected schema.Block } tests := map[string]testCase{ - "empty-opts": { - opts: datasourcetimeouts.Opts{}, - expected: schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ - ObjectType: types.ObjectType{ - AttrTypes: map[string]attr.Type{}, - }, - }, - Attributes: map[string]schema.Attribute{}, - }, - }, - "create-opts": { - opts: datasourcetimeouts.Opts{ - Create: true, - }, + "read": { expected: schema.SingleNestedBlock{ CustomType: timeouts.TimeoutsType{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ - "create": types.StringType, + "read": types.StringType, }, }, }, Attributes: map[string]schema.Attribute{ - "create": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - }, - }, - }, - "create-update-opts": { - opts: datasourcetimeouts.Opts{ - Create: true, - Update: true, - }, - expected: schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ - ObjectType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "create": types.StringType, - "update": types.StringType, - }, - }, - }, - Attributes: map[string]schema.Attribute{ - "create": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "update": schema.StringAttribute{ + "read": schema.StringAttribute{ Optional: true, Validators: []validator.String{ validators.TimeDurationString(), @@ -91,7 +46,7 @@ func TestBlock(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - actual := datasourcetimeouts.Block(context.Background(), test.opts) + actual := datasourcetimeouts.Block(context.Background()) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected block difference: %s", diff) @@ -100,82 +55,17 @@ func TestBlock(t *testing.T) { } } -func TestBlockAll(t *testing.T) { - t.Parallel() - - actual := datasourcetimeouts.BlockAll(context.Background()) - - expected := schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ - ObjectType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "create": types.StringType, - "read": types.StringType, - "update": types.StringType, - "delete": types.StringType, - }, - }, - }, - Attributes: map[string]schema.Attribute{ - "create": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "read": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "update": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "delete": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - }, - } - - if diff := cmp.Diff(actual, expected); diff != "" { - t.Errorf("unexpected block difference: %s", diff) - } -} - func TestAttributes(t *testing.T) { t.Parallel() type testCase struct { - opts datasourcetimeouts.Opts expected schema.Attribute } tests := map[string]testCase{ - "empty-opts": { - opts: datasourcetimeouts.Opts{}, - expected: schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{}, - CustomType: timeouts.TimeoutsType{ - ObjectType: types.ObjectType{ - AttrTypes: map[string]attr.Type{}, - }, - }, - Optional: true, - }, - }, - "create-opts": { - opts: datasourcetimeouts.Opts{ - Create: true, - }, + "read": { expected: schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ - "create": schema.StringAttribute{ + "read": schema.StringAttribute{ Optional: true, Validators: []validator.String{ validators.TimeDurationString(), @@ -185,38 +75,7 @@ func TestAttributes(t *testing.T) { CustomType: timeouts.TimeoutsType{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ - "create": types.StringType, - }, - }, - }, - Optional: true, - }, - }, - "create-update-opts": { - opts: datasourcetimeouts.Opts{ - Create: true, - Update: true, - }, - expected: schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "create": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "update": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - }, - CustomType: timeouts.TimeoutsType{ - ObjectType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "create": types.StringType, - "update": types.StringType, + "read": types.StringType, }, }, }, @@ -228,7 +87,7 @@ func TestAttributes(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - actual := datasourcetimeouts.Attributes(context.Background(), test.opts) + actual := datasourcetimeouts.Attributes(context.Background()) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected block difference: %s", diff) @@ -236,53 +95,3 @@ func TestAttributes(t *testing.T) { }) } } - -func TestAttributesAll(t *testing.T) { - t.Parallel() - - actual := datasourcetimeouts.AttributesAll(context.Background()) - - expected := schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "create": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "read": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "update": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - "delete": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - validators.TimeDurationString(), - }, - }, - }, - CustomType: timeouts.TimeoutsType{ - ObjectType: types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "create": types.StringType, - "read": types.StringType, - "update": types.StringType, - "delete": types.StringType, - }, - }, - }, - Optional: true, - } - - if diff := cmp.Diff(actual, expected); diff != "" { - t.Errorf("unexpected block difference: %s", diff) - } -} From 7e640efbdee6d4fc5d2bab2d86268492e8ae4f99 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 13 Dec 2022 12:56:43 +0000 Subject: [PATCH 09/17] Moving timeouts Type to top-level and returning diags from get timeouts functions (#17) --- timeouts/datasource/schema.go | 2 +- timeouts/datasource/schema_test.go | 2 +- timeouts/resource/schema.go | 2 +- timeouts/resource/schema_test.go | 2 +- timeouts/schema.go | 7 --- timeouts/{type => }/timeouts.go | 37 +++++++++---- timeouts/{type => }/timeouts_test.go | 83 +++++++++++++++++++++------- 7 files changed, 91 insertions(+), 44 deletions(-) rename timeouts/{type => }/timeouts.go (75%) rename timeouts/{type => }/timeouts_test.go (81%) diff --git a/timeouts/datasource/schema.go b/timeouts/datasource/schema.go index 9dfb796..264576f 100644 --- a/timeouts/datasource/schema.go +++ b/timeouts/datasource/schema.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - timeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" ) const ( diff --git a/timeouts/datasource/schema_test.go b/timeouts/datasource/schema_test.go index 277b5f8..4739505 100644 --- a/timeouts/datasource/schema_test.go +++ b/timeouts/datasource/schema_test.go @@ -11,8 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" datasourcetimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" ) func TestBlock(t *testing.T) { diff --git a/timeouts/resource/schema.go b/timeouts/resource/schema.go index 482e6a4..f90864d 100644 --- a/timeouts/resource/schema.go +++ b/timeouts/resource/schema.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" ) const ( diff --git a/timeouts/resource/schema_test.go b/timeouts/resource/schema_test.go index e8a4da8..4802c98 100644 --- a/timeouts/resource/schema_test.go +++ b/timeouts/resource/schema_test.go @@ -11,8 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" resourcetimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" ) func TestBlock(t *testing.T) { diff --git a/timeouts/schema.go b/timeouts/schema.go index 46446cb..8362cbb 100644 --- a/timeouts/schema.go +++ b/timeouts/schema.go @@ -9,13 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" ) -const ( - attributeNameCreate = "create" - attributeNameRead = "read" - attributeNameUpdate = "update" - attributeNameDelete = "delete" -) - // Opts is used as an argument to Block and Attributes to indicate which attributes // should be created. type Opts struct { diff --git a/timeouts/type/timeouts.go b/timeouts/timeouts.go similarity index 75% rename from timeouts/type/timeouts.go rename to timeouts/timeouts.go index 2312c38..20d7816 100644 --- a/timeouts/type/timeouts.go +++ b/timeouts/timeouts.go @@ -6,6 +6,7 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -80,41 +81,53 @@ func (t TimeoutsValue) Type(ctx context.Context) attr.Type { } // Create attempts to retrieve the "create" attribute and parse it as time.Duration. -// If any errors are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Create(ctx context.Context) (time.Duration, error) { +// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Create(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameCreate) } // Read attempts to retrieve the "read" attribute and parse it as time.Duration. -// If any errors are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Read(ctx context.Context) (time.Duration, error) { +// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Read(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameRead) } // Update attempts to retrieve the "update" attribute and parse it as time.Duration. -// If any errors are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Update(ctx context.Context) (time.Duration, error) { +// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Update(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameUpdate) } // Delete attempts to retrieve the "delete" attribute and parse it as time.Duration. -// If any errors are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Delete(ctx context.Context) (time.Duration, error) { +// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. +func (t TimeoutsValue) Delete(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameDelete) } -func (t TimeoutsValue) getTimeout(_ context.Context, timeoutName string) (time.Duration, error) { +func (t TimeoutsValue) getTimeout(_ context.Context, timeoutName string) (time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + value, ok := t.Object.Attributes()[timeoutName] if !ok { - return defaultTimeout, fmt.Errorf("timeout for %q does not exist", timeoutName) + diags.Append(diag.NewErrorDiagnostic( + "Timeout Does Not Exist", + fmt.Sprintf("timeout for %q does not exist", timeoutName), + )) + + return defaultTimeout, diags } // No type assertion check is required as the schema guarantees that the object attributes // are types.String. createTimeout, err := time.ParseDuration(value.(types.String).ValueString()) if err != nil { - return defaultTimeout, err + diags.Append(diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + fmt.Sprintf("timeout for %q cannot be parsed, %s", timeoutName, err), + )) + + return defaultTimeout, diags } - return createTimeout, nil + return createTimeout, diags } diff --git a/timeouts/type/timeouts_test.go b/timeouts/timeouts_test.go similarity index 81% rename from timeouts/type/timeouts_test.go rename to timeouts/timeouts_test.go index 37f7c1a..d1f396c 100644 --- a/timeouts/type/timeouts_test.go +++ b/timeouts/timeouts_test.go @@ -8,10 +8,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/type" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" ) func TestTimeoutsTypeValueFromTerraform(t *testing.T) { @@ -157,7 +158,7 @@ func TestTimeoutsValueCreate(t *testing.T) { type testCase struct { timeoutsValue timeouts.TimeoutsValue expectedTimeout time.Duration - expectedErr error + expectedDiags diag.Diagnostics } tests := map[string]testCase{ "create": { @@ -172,14 +173,19 @@ func TestTimeoutsValueCreate(t *testing.T) { ), }, expectedTimeout: 10 * time.Minute, - expectedErr: nil, + expectedDiags: nil, }, "create-not-set": { timeoutsValue: timeouts.TimeoutsValue{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`timeout for "create" does not exist`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Does Not Exist", + fmt.Sprintf(`timeout for "create" does not exist`), + ), + }, }, "create-not-parseable-as-time-duration": { timeoutsValue: timeouts.TimeoutsValue{ @@ -193,7 +199,12 @@ func TestTimeoutsValueCreate(t *testing.T) { ), }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + fmt.Sprintf(`timeout for "create" cannot be parsed, time: unknown unit "x" in duration "10x"`), + ), + }, }, } @@ -208,7 +219,7 @@ func TestTimeoutsValueCreate(t *testing.T) { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) @@ -221,7 +232,7 @@ func TestTimeoutsValueRead(t *testing.T) { type testCase struct { timeoutsValue timeouts.TimeoutsValue expectedTimeout time.Duration - expectedErr error + expectedDiags diag.Diagnostics } tests := map[string]testCase{ "read": { @@ -236,14 +247,19 @@ func TestTimeoutsValueRead(t *testing.T) { ), }, expectedTimeout: 10 * time.Minute, - expectedErr: nil, + expectedDiags: nil, }, "read-not-set": { timeoutsValue: timeouts.TimeoutsValue{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`timeout for "read" does not exist`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Does Not Exist", + fmt.Sprintf(`timeout for "read" does not exist`), + ), + }, }, "read-not-parseable-as-time-duration": { timeoutsValue: timeouts.TimeoutsValue{ @@ -257,7 +273,12 @@ func TestTimeoutsValueRead(t *testing.T) { ), }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + fmt.Sprintf(`timeout for "read" cannot be parsed, time: unknown unit "x" in duration "10x"`), + ), + }, }, } @@ -272,7 +293,7 @@ func TestTimeoutsValueRead(t *testing.T) { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) @@ -285,7 +306,7 @@ func TestTimeoutsValueUpdate(t *testing.T) { type testCase struct { timeoutsValue timeouts.TimeoutsValue expectedTimeout time.Duration - expectedErr error + expectedDiags diag.Diagnostics } tests := map[string]testCase{ "update": { @@ -300,14 +321,19 @@ func TestTimeoutsValueUpdate(t *testing.T) { ), }, expectedTimeout: 10 * time.Minute, - expectedErr: nil, + expectedDiags: nil, }, "update-not-set": { timeoutsValue: timeouts.TimeoutsValue{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`timeout for "update" does not exist`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Does Not Exist", + fmt.Sprintf(`timeout for "update" does not exist`), + ), + }, }, "update-not-parseable-as-time-duration": { timeoutsValue: timeouts.TimeoutsValue{ @@ -321,7 +347,12 @@ func TestTimeoutsValueUpdate(t *testing.T) { ), }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + fmt.Sprintf(`timeout for "update" cannot be parsed, time: unknown unit "x" in duration "10x"`), + ), + }, }, } @@ -336,7 +367,7 @@ func TestTimeoutsValueUpdate(t *testing.T) { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) @@ -349,7 +380,7 @@ func TestTimeoutsValueDelete(t *testing.T) { type testCase struct { timeoutsValue timeouts.TimeoutsValue expectedTimeout time.Duration - expectedErr error + expectedDiags diag.Diagnostics } tests := map[string]testCase{ "delete": { @@ -364,14 +395,19 @@ func TestTimeoutsValueDelete(t *testing.T) { ), }, expectedTimeout: 10 * time.Minute, - expectedErr: nil, + expectedDiags: nil, }, "delete-not-set": { timeoutsValue: timeouts.TimeoutsValue{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`timeout for "delete" does not exist`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Does Not Exist", + fmt.Sprintf(`timeout for "delete" does not exist`), + ), + }, }, "delete-not-parseable-as-time-duration": { timeoutsValue: timeouts.TimeoutsValue{ @@ -385,7 +421,12 @@ func TestTimeoutsValueDelete(t *testing.T) { ), }, expectedTimeout: 20 * time.Minute, - expectedErr: fmt.Errorf(`time: unknown unit "x" in duration "10x"`), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + fmt.Sprintf(`timeout for "delete" cannot be parsed, time: unknown unit "x" in duration "10x"`), + ), + }, }, } @@ -400,7 +441,7 @@ func TestTimeoutsValueDelete(t *testing.T) { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedErr, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) From 7c65b9a955445791f28120fcc95acb36aac13783 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 13 Dec 2022 13:00:04 +0000 Subject: [PATCH 10/17] Linting (#17) --- timeouts/timeouts_test.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/timeouts/timeouts_test.go b/timeouts/timeouts_test.go index d1f396c..098aa0d 100644 --- a/timeouts/timeouts_test.go +++ b/timeouts/timeouts_test.go @@ -2,7 +2,6 @@ package timeouts_test import ( "context" - "fmt" "testing" "time" @@ -183,7 +182,7 @@ func TestTimeoutsValueCreate(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Does Not Exist", - fmt.Sprintf(`timeout for "create" does not exist`), + `timeout for "create" does not exist`, ), }, }, @@ -202,7 +201,7 @@ func TestTimeoutsValueCreate(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Cannot Be Parsed", - fmt.Sprintf(`timeout for "create" cannot be parsed, time: unknown unit "x" in duration "10x"`), + `timeout for "create" cannot be parsed, time: unknown unit "x" in duration "10x"`, ), }, }, @@ -257,7 +256,7 @@ func TestTimeoutsValueRead(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Does Not Exist", - fmt.Sprintf(`timeout for "read" does not exist`), + `timeout for "read" does not exist`, ), }, }, @@ -276,7 +275,7 @@ func TestTimeoutsValueRead(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Cannot Be Parsed", - fmt.Sprintf(`timeout for "read" cannot be parsed, time: unknown unit "x" in duration "10x"`), + `timeout for "read" cannot be parsed, time: unknown unit "x" in duration "10x"`, ), }, }, @@ -331,7 +330,7 @@ func TestTimeoutsValueUpdate(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Does Not Exist", - fmt.Sprintf(`timeout for "update" does not exist`), + `timeout for "update" does not exist`, ), }, }, @@ -350,7 +349,7 @@ func TestTimeoutsValueUpdate(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Cannot Be Parsed", - fmt.Sprintf(`timeout for "update" cannot be parsed, time: unknown unit "x" in duration "10x"`), + `timeout for "update" cannot be parsed, time: unknown unit "x" in duration "10x"`, ), }, }, @@ -405,7 +404,7 @@ func TestTimeoutsValueDelete(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Does Not Exist", - fmt.Sprintf(`timeout for "delete" does not exist`), + `timeout for "delete" does not exist`, ), }, }, @@ -424,7 +423,7 @@ func TestTimeoutsValueDelete(t *testing.T) { expectedDiags: diag.Diagnostics{ diag.NewErrorDiagnostic( "Timeout Cannot Be Parsed", - fmt.Sprintf(`timeout for "delete" cannot be parsed, time: unknown unit "x" in duration "10x"`), + `timeout for "delete" cannot be parsed, time: unknown unit "x" in duration "10x"`, ), }, }, From 5f87fdbd9d077d006b0539ae7143c6ad23bc5708 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 13 Dec 2022 14:27:34 +0000 Subject: [PATCH 11/17] Removing usage of tfsdk.Block and tfsdk.Attribute (#17) --- .changelog/18.txt | 16 ++ timeouts/parser.go | 136 ---------------- timeouts/parser_test.go | 349 ---------------------------------------- timeouts/schema.go | 113 ------------- timeouts/schema_test.go | 247 ---------------------------- 5 files changed, 16 insertions(+), 845 deletions(-) delete mode 100644 timeouts/parser.go delete mode 100644 timeouts/parser_test.go delete mode 100644 timeouts/schema.go delete mode 100644 timeouts/schema_test.go diff --git a/.changelog/18.txt b/.changelog/18.txt index 5696e44..730b686 100644 --- a/.changelog/18.txt +++ b/.changelog/18.txt @@ -9,3 +9,19 @@ Introduced resource `timeouts` package for use with resource schema ```release-note:feature Introduced type `timeouts` package to provider `TimeoutsType` and `TimeoutsValue` for use with schema and data models, respectively ``` + +```release-note:breaking-change +all: The `Block() tfsdk.Block` method has been removed. Use the resource `Block() schema.Block` or data source `Block() schema.Block` function instead. +``` + +```release-note:breaking-change +all: The `BlockAll() tfsdk.Block` method has been removed. Use the resource `BlockAll() schema.Block` or data source `Block() schema.Block` function instead. +``` + +```release-note:breaking-change +all: The `Attributes() tfsdk.Attribute` method has been removed. Use the resource `Attributes() schema.Attribute` or data source `Attributes() schema.Attribute` function instead. +``` + +```release-note:breaking-change +all: The `AttributesAll() tfsdk.Attribute` method has been removed. Use the resource `AttributesAll() schema.Attribute` or data source `Attributes() schema.Attribute` function instead. +``` diff --git a/timeouts/parser.go b/timeouts/parser.go deleted file mode 100644 index 0068356..0000000 --- a/timeouts/parser.go +++ /dev/null @@ -1,136 +0,0 @@ -package timeouts - -import ( - "context" - "time" - - "github.com/hashicorp/terraform-plugin-framework/types" -) - -// Create interrogates the supplied types.Object and if the object.Attrs contains an -// entry for "create" that can be parsed then time.Duration is returned. If object.Attrs -// does not contain "create" the supplied default will be returned. -func Create(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - createTimeoutValue, ok := obj.Attributes()[attributeNameCreate] - - if !ok { - return def - } - - if createTimeoutValue.IsNull() { - return def - } - - // Although the schema mutation functions guarantee that the type for create timeout - // is a string, this function accepts any types.Object. - createTimeoutString, ok := createTimeoutValue.(types.String) - - if !ok { - return def - } - - // Although the schema validation guarantees that the type for create timeout - // is parseable as a time.Duration, this function accepts any types.Object. - duration, err := time.ParseDuration(createTimeoutString.ValueString()) - if err != nil { - return def - } - - return duration -} - -// Read interrogates the supplied types.Object and if the object.Attrs contains an -// entry for "read" that can be parsed then time.Duration is returned. If object.Attrs -// does not contain "read" the supplied default will be returned. -func Read(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - readTimeoutValue, ok := obj.Attributes()[attributeNameRead] - - if !ok { - return def - } - - if readTimeoutValue.IsNull() { - return def - } - - // Although the schema mutation functions guarantee that the type for read timeout - // is a string, this function accepts any types.Object. - readTimeoutString, ok := readTimeoutValue.(types.String) - - if !ok { - return def - } - - // Although the schema validation guarantees that the type for read timeout - // is parseable as a time.Duration, this function accepts any types.Object. - duration, err := time.ParseDuration(readTimeoutString.ValueString()) - if err != nil { - return def - } - - return duration -} - -// Update interrogates the supplied types.Object and if the object.Attrs contains an -// entry for "update" that can be parsed then time.Duration is returned. If object.Attrs -// does not contain "update" the supplied default will be returned. -func Update(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - updateTimeoutValue, ok := obj.Attributes()[attributeNameUpdate] - - if !ok { - return def - } - - if updateTimeoutValue.IsNull() { - return def - } - - // Although the schema mutation functions guarantee that the type for update timeout - // is a string, this function accepts any types.Object. - updateTimeoutString, ok := updateTimeoutValue.(types.String) - - if !ok { - return def - } - - // Although the schema validation guarantees that the type for update timeout - // is parseable as a time.Duration, this function accepts any types.Object. - duration, err := time.ParseDuration(updateTimeoutString.ValueString()) - if err != nil { - return def - } - - return duration -} - -// Delete interrogates the supplied types.Object and if the object.Attrs contains an -// entry for "delete" that can be parsed then time.Duration is returned. If object.Attrs -// does not contain "delete" the supplied default will be returned. -func Delete(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - deleteTimeoutValue, ok := obj.Attributes()[attributeNameDelete] - - if !ok { - return def - } - - if deleteTimeoutValue.IsNull() { - return def - } - - // Although the schema mutation functions guarantee that the type for delete timeout - // is a string, this function accepts any types.Object. - deleteTimeoutString, ok := deleteTimeoutValue.(types.String) - - if !ok { - return def - } - - // Although the schema validation guarantees that the type for delete timeout - // is parseable as a time.Duration, this function accepts any types.Object. - duration, err := time.ParseDuration(deleteTimeoutString.ValueString()) - if err != nil { - return def - } - - return duration -} diff --git a/timeouts/parser_test.go b/timeouts/parser_test.go deleted file mode 100644 index b3364ea..0000000 --- a/timeouts/parser_test.go +++ /dev/null @@ -1,349 +0,0 @@ -package timeouts_test - -import ( - "context" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" -) - -func TestCreate(t *testing.T) { - t.Parallel() - - def := 20 * time.Minute - - type testCase struct { - obj types.Object - expected time.Duration - } - - tests := map[string]testCase{ - "create-not-present": { - obj: types.Object{}, - expected: def, - }, - "create-is-null": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "create": types.StringType, - }, - map[string]attr.Value{ - "create": types.StringNull(), - }, - ), - expected: def, - }, - "create-not-string": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "create": types.BoolType, - }, - map[string]attr.Value{ - "create": types.Bool{}, - }, - ), - expected: def, - }, - "create-not-parseable-empty": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "create": types.StringType, - }, - map[string]attr.Value{ - "create": types.StringValue(""), - }, - ), - expected: def, - }, - "create-not-parseable": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "create": types.StringType, - }, - map[string]attr.Value{ - "create": types.StringValue("60x"), - }, - ), - expected: def, - }, - "create-valid": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "create": types.StringType, - }, - map[string]attr.Value{ - "create": types.StringValue("60m"), - }, - ), - expected: 60 * time.Minute, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.Create(context.Background(), test.obj, def) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - }) - } -} - -func TestRead(t *testing.T) { - t.Parallel() - - def := 20 * time.Minute - - type testCase struct { - obj types.Object - expected time.Duration - } - - tests := map[string]testCase{ - "read-not-present": { - obj: types.Object{}, - expected: def, - }, - "read-is-null": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "read": types.StringType, - }, - map[string]attr.Value{ - "read": types.StringNull(), - }, - ), - expected: def, - }, - "read-not-string": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "read": types.BoolType, - }, - map[string]attr.Value{ - "read": types.BoolValue(true), - }, - ), - expected: def, - }, - "read-not-parseable-empty": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "read": types.StringType, - }, - map[string]attr.Value{ - "read": types.StringValue(""), - }, - ), - expected: def, - }, - "read-not-parseable": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "read": types.StringType, - }, - map[string]attr.Value{ - "read": types.StringValue("60x"), - }, - ), - expected: def, - }, - "read-valid": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "read": types.StringType, - }, - map[string]attr.Value{ - "read": types.StringValue("60m"), - }, - ), - expected: 60 * time.Minute, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.Read(context.Background(), test.obj, def) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - }) - } -} - -func TestUpdate(t *testing.T) { - t.Parallel() - - def := 20 * time.Minute - - type testCase struct { - obj types.Object - expected time.Duration - } - - tests := map[string]testCase{ - "update-not-present": { - obj: types.Object{}, - expected: def, - }, - "update-is-null": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "update": types.StringType, - }, - map[string]attr.Value{ - "update": types.StringNull(), - }, - ), - expected: def, - }, - "update-not-string": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "update": types.BoolType, - }, - map[string]attr.Value{ - "update": types.BoolValue(true), - }, - ), - expected: def, - }, - "update-not-parseable-empty": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "update": types.StringType, - }, - map[string]attr.Value{ - "update": types.StringValue(""), - }, - ), - expected: def, - }, - "update-not-parseable": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "update": types.StringType, - }, - map[string]attr.Value{ - "update": types.StringValue("60x"), - }, - ), - expected: def, - }, - "update-valid": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "update": types.StringType, - }, - map[string]attr.Value{ - "update": types.StringValue("60m"), - }, - ), - expected: 60 * time.Minute, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.Update(context.Background(), test.obj, def) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - }) - } -} - -func TestDelete(t *testing.T) { - t.Parallel() - - def := 20 * time.Minute - - type testCase struct { - obj types.Object - expected time.Duration - } - - tests := map[string]testCase{ - "delete-not-present": { - obj: types.Object{}, - expected: def, - }, - "delete-is-null": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "delete": types.StringType, - }, - map[string]attr.Value{ - "delete": types.StringNull(), - }, - ), - expected: def, - }, - "delete-not-string": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "delete": types.BoolType, - }, - map[string]attr.Value{ - "delete": types.BoolValue(true), - }, - ), - expected: def, - }, - "delete-not-parseable-empty": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "delete": types.StringType, - }, - map[string]attr.Value{ - "delete": types.StringValue(""), - }, - ), - expected: def, - }, - "delete-not-parseable": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "delete": types.StringType, - }, - map[string]attr.Value{ - "delete": types.StringValue("60x"), - }, - ), - expected: def, - }, - "delete-valid": { - obj: types.ObjectValueMust( - map[string]attr.Type{ - "delete": types.StringType, - }, - map[string]attr.Value{ - "delete": types.StringValue("60m"), - }, - ), - expected: 60 * time.Minute, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.Delete(context.Background(), test.obj, def) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - }) - } -} diff --git a/timeouts/schema.go b/timeouts/schema.go deleted file mode 100644 index 8362cbb..0000000 --- a/timeouts/schema.go +++ /dev/null @@ -1,113 +0,0 @@ -package timeouts - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" -) - -// Opts is used as an argument to Block and Attributes to indicate which attributes -// should be created. -type Opts struct { - Create bool - Read bool - Update bool - Delete bool -} - -// Block returns a tfsdk.Block containing attributes for each of the fields -// in Opts which are set to true. Each attribute is defined as types.StringType -// and optional. A validator is used to verify that the value assigned to an -// attribute can be parsed as time.Duration. -// -// Deprecated: Use resourcetimeouts.Block or datasourcetimeouts.Block instead. -// -//nolint:staticcheck -func Block(ctx context.Context, opts Opts) tfsdk.Block { - return tfsdk.Block{ - Attributes: attributesMap(opts), - NestingMode: tfsdk.BlockNestingModeSingle, - } -} - -// BlockAll returns a tfsdk.Block containing attributes for each of create, read, -// update and delete. Each attribute is defined as types.StringType and optional. -// A validator is used to verify that the value assigned to an attribute can be -// parsed as time.Duration. -// -// Deprecated: Use resourcetimeouts.BlockAll or datasourcetimeouts.BlockAll instead. -// -//nolint:staticcheck -func BlockAll(ctx context.Context) tfsdk.Block { - return Block(ctx, Opts{ - Create: true, - Read: true, - Update: true, - Delete: true, - }) -} - -// Attributes returns a tfsdk.Attribute containing a tfsdk.SingleNestedAttributes -// which contains attributes for each of the fields in Opts which are set to true. -// Each attribute is defined as types.StringType and optional. A validator is used -// to verify that the value assigned to an attribute can be parsed as time.Duration. -// -// Deprecated: Use resourcetimeouts.Attributes or datasourcetimeouts.Attributes instead. -// -//nolint:staticcheck -func Attributes(ctx context.Context, opts Opts) tfsdk.Attribute { - return tfsdk.Attribute{ - Optional: true, - Attributes: tfsdk.SingleNestedAttributes(attributesMap(opts)), - } -} - -// AttributesAll returns a tfsdk.Attribute containing a tfsdk.SingleNestedAttributes -// which contains attributes for each of create, read, update and delete. Each -// attribute is defined as types.StringType and optional. A validator is used to -// verify that the value assigned to an attribute can be parsed as time.Duration. -// -// Deprecated: Use resourcetimeouts.AttributesAll or datasourcetimeouts.AttributesAll instead. -// -//nolint:staticcheck -func AttributesAll(ctx context.Context) tfsdk.Attribute { - return Attributes(ctx, Opts{ - Create: true, - Read: true, - Update: true, - Delete: true, - }) -} - -//nolint:staticcheck -func attributesMap(opts Opts) map[string]tfsdk.Attribute { - attributes := map[string]tfsdk.Attribute{} - attribute := tfsdk.Attribute{ - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - } - - if opts.Create { - attributes[attributeNameCreate] = attribute - } - - if opts.Read { - attributes[attributeNameRead] = attribute - } - - if opts.Update { - attributes[attributeNameUpdate] = attribute - } - - if opts.Delete { - attributes[attributeNameDelete] = attribute - } - - return attributes -} diff --git a/timeouts/schema_test.go b/timeouts/schema_test.go deleted file mode 100644 index 1f1c514..0000000 --- a/timeouts/schema_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package timeouts_test - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" -) - -//nolint:staticcheck -func TestBlock(t *testing.T) { - t.Parallel() - - type testCase struct { - opts timeouts.Opts - expected tfsdk.Block - } - tests := map[string]testCase{ - "empty-opts": { - opts: timeouts.Opts{}, - expected: tfsdk.Block{ - Attributes: map[string]tfsdk.Attribute{}, - NestingMode: tfsdk.BlockNestingModeSingle, - }, - }, - "create-opts": { - opts: timeouts.Opts{ - Create: true, - }, - expected: tfsdk.Block{ - Attributes: map[string]tfsdk.Attribute{ - "create": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - }, - NestingMode: tfsdk.BlockNestingModeSingle, - }, - }, - "create-update-opts": { - opts: timeouts.Opts{ - Create: true, - Update: true, - }, - expected: tfsdk.Block{ - Attributes: map[string]tfsdk.Attribute{ - "create": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "update": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - }, - NestingMode: tfsdk.BlockNestingModeSingle, - }, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.Block(context.Background(), test.opts) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected block difference: %s", diff) - } - }) - } -} - -//nolint:staticcheck -func TestBlockAll(t *testing.T) { - t.Parallel() - - actual := timeouts.BlockAll(context.Background()) - - expected := tfsdk.Block{ - Attributes: map[string]tfsdk.Attribute{ - "create": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "read": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "update": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "delete": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - }, - NestingMode: tfsdk.BlockNestingModeSingle, - } - - if diff := cmp.Diff(actual, expected); diff != "" { - t.Errorf("unexpected block difference: %s", diff) - } -} - -//nolint:staticcheck -func TestAttributes(t *testing.T) { - t.Parallel() - - type testCase struct { - opts timeouts.Opts - expected tfsdk.Attribute - } - tests := map[string]testCase{ - "empty-opts": { - opts: timeouts.Opts{}, - expected: tfsdk.Attribute{ - Optional: true, - Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{}), - }, - }, - "create-opts": { - opts: timeouts.Opts{ - Create: true, - }, - expected: tfsdk.Attribute{ - Optional: true, - Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ - "create": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - }), - }, - }, - "create-update-opts": { - opts: timeouts.Opts{ - Create: true, - Update: true, - }, - expected: tfsdk.Attribute{ - Optional: true, - Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ - "create": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "update": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - }), - }, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.Attributes(context.Background(), test.opts) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected block difference: %s", diff) - } - }) - } -} - -//nolint:staticcheck -func TestAttributesAll(t *testing.T) { - t.Parallel() - - actual := timeouts.AttributesAll(context.Background()) - - expected := tfsdk.Attribute{ - Optional: true, - Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ - "create": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "read": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "update": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - "delete": { - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - }, - }), - } - - if diff := cmp.Diff(actual, expected); diff != "" { - t.Errorf("unexpected block difference: %s", diff) - } -} From 60a4f841df9c85a92fcb35a9a8966a12e5ac8cf6 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Dec 2022 07:30:31 +0000 Subject: [PATCH 12/17] Removing all usages of tfsdk package and separating type and value into resource and data source specific implementations (#17) --- README.md | 113 +++++++++--- internal/validators/timeduration.go | 35 +--- internal/validators/timeduration_test.go | 53 +----- timeouts/datasource/schema.go | 7 +- timeouts/datasource/schema_test.go | 15 +- timeouts/datasource/timeouts.go | 109 +++++++++++ timeouts/datasource/timeouts_test.go | 220 +++++++++++++++++++++++ timeouts/resource/schema.go | 7 +- timeouts/resource/schema_test.go | 71 ++++---- timeouts/{ => resource}/timeouts.go | 58 +++--- timeouts/{ => resource}/timeouts_test.go | 50 +++--- 11 files changed, 523 insertions(+), 215 deletions(-) create mode 100644 timeouts/datasource/timeouts.go create mode 100644 timeouts/datasource/timeouts_test.go rename timeouts/{ => resource}/timeouts.go (57%) rename timeouts/{ => resource}/timeouts_test.go (89%) diff --git a/README.md b/README.md index 564e967..fb230a9 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ Terraform configuration. #### Block -If your configuration is using a nested block to define timeouts, such as the following: +The following illustrates nested block syntax for defining timeouts on a resource and a data source. -```terraform +```hcl resource "timeouts_example" "example" { /* ... */ @@ -48,25 +48,63 @@ resource "timeouts_example" "example" { } ``` -You can use this module to mutate the `schema.Schema` as follows: +```hcl +data "timeouts_example" "example" { + /* ... */ + + timeouts { + read = "30m" + } +} +``` + +Use this module to mutate the `schema.Schema`: + +You must supply `timeouts.Opts` when calling `timeouts.Block()` on a resource. Alternatively, `timeouts.BlockAll()` will generate attributes for `create`, `read`, `update` and `delete`. ```go +import ( + /* ... */ + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" +) + func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - return tfsdk.Schema{ + resp.Schema = schema.Schema{ /* ... */ + + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, + timeouts.Opts{ + Create: true, + }, + ) + }, +``` - Blocks: map[string]tfsdk.Block{ - "timeouts": timeouts.Block(ctx, timeouts.Opts{ - Create: true, - }), +The `timeouts.Block()` call does not accept options on a data source as `read` is the only option. + +```go +import ( + /* ... */ + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" +) + +func (t exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + /* ... */ + + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx), }, + } +} ``` #### Attribute -If your configuration is using nested attributes to define timeouts, such as the following: +The following illustrates nested attribute syntax for defining timeouts on a resource and a data source. -```terraform +```hcl resource "timeouts_example" "example" { /* ... */ @@ -76,12 +114,29 @@ resource "timeouts_example" "example" { } ``` -You can use this module to mutate the `schema.Schema` as follows: +```hcl +data "timeouts_example" "example" { + /* ... */ + + timeouts = { + read = "30m" + } +} +``` + +Use this module to mutate the `schema.Schema` as follows: + +You must supply `timeouts.Opts` when calling `timeouts.Attributes()` on a resource. ```go +import ( + /* ... */ + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" +) + func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ /* ... */ "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ Create: true, @@ -89,25 +144,43 @@ func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest }, ``` +The `timeouts.Attributes()` call does not accept options on a data source as `read` is the only option. + +```go +import ( + /* ... */ + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" +) + +func (t exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + /* ... */ + "timeouts": timeouts.Attributes(ctx), + }, + } +} +``` + ### Updating Models In functions in which the config, state or plan is being unmarshalled, for instance, the `Create` function: ```go func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data exampleResourceData + var data exampleResourceData - diags := req.Plan.Get(ctx, &data) - resp.Diagnostics.Append(diags...) + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) ``` The model that is being used, `exampleResourceData` in this example, will need to be modified to include a field for -timeouts which is of type `timeouts.TimeoutsValue`. For example: +timeouts which is of type `timeouts.Value`. For example: ```go type exampleResourceData struct { /* ... */ - Timeouts timeouts.TimeoutsValue `tfsdk:"timeouts"` + Timeouts timeouts.Value `tfsdk:"timeouts"` ``` ### Accessing Timeouts in CRUD Functions @@ -129,10 +202,10 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, if err != nil { // handle error } - + ctx, cancel := context.WithTimeout(ctx, createTimeout) defer cancel() - + /* ... */ } ``` diff --git a/internal/validators/timeduration.go b/internal/validators/timeduration.go index 23d9a38..c00b772 100644 --- a/internal/validators/timeduration.go +++ b/internal/validators/timeduration.go @@ -7,11 +7,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" ) -var _ tfsdk.AttributeValidator = timeDurationValidator{} var _ validator.String = timeDurationValidator{} // timeDurationValidator validates that a string Attribute's value is parseable as time.Duration. @@ -28,26 +25,6 @@ func (validator timeDurationValidator) MarkdownDescription(ctx context.Context) return validator.Description(ctx) } -// Validate performs the validation. -// -// Deprecated: Use ValidateString instead. -func (validator timeDurationValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { - s := request.AttributeConfig.(types.String) - - if s.IsUnknown() || s.IsNull() { - return - } - - if _, err := time.ParseDuration(s.ValueString()); err != nil { - response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( - request.AttributePath, - "Invalid Attribute Value Time Duration", - fmt.Sprintf("%q %s", s.ValueString(), validator.Description(ctx))), - ) - return - } -} - // ValidateString performs the validation. func (validator timeDurationValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { s := req.ConfigValue @@ -72,16 +49,6 @@ func (validator timeDurationValidator) ValidateString(ctx context.Context, req v // - Is parseable as time duration. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func TimeDuration() tfsdk.AttributeValidator { - return timeDurationValidator{} -} - -// TimeDurationString returns an AttributeValidator which ensures that any configured -// attribute value: -// -// - Is parseable as time duration. -// -// Null (unconfigured) and unknown (known after apply) values are skipped. -func TimeDurationString() validator.String { +func TimeDuration() validator.String { return timeDurationValidator{} } diff --git a/internal/validators/timeduration_test.go b/internal/validators/timeduration_test.go index 6a81e50..5d629aa 100644 --- a/internal/validators/timeduration_test.go +++ b/internal/validators/timeduration_test.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" @@ -44,56 +43,6 @@ func TestTimeDuration(t *testing.T) { }, } - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - request := tfsdk.ValidateAttributeRequest{ - AttributePath: path.Root("test"), - AttributePathExpression: path.MatchRoot("test"), - AttributeConfig: test.val, - } - - response := tfsdk.ValidateAttributeResponse{} - - validators.TimeDuration().Validate(context.Background(), request, &response) - - if diff := cmp.Diff(response.Diagnostics, test.expectedDiagnostics); diff != "" { - t.Errorf("unexpected diagnostics difference: %s", diff) - } - }) - } -} - -func TestTimeDurationString(t *testing.T) { - t.Parallel() - - type testCase struct { - val types.String - expectedDiagnostics diag.Diagnostics - } - - tests := map[string]testCase{ - "unknown": { - val: types.StringUnknown(), - }, - "null": { - val: types.StringNull(), - }, - "valid": { - val: types.StringValue("20m"), - }, - "invalid": { - val: types.StringValue("20x"), - expectedDiagnostics: diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic( - path.Root("test"), - "Invalid Attribute Value Time Duration", - `"20x" must be a string containing a sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".`, - ), - }, - }, - } - for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { @@ -105,7 +54,7 @@ func TestTimeDurationString(t *testing.T) { response := validator.StringResponse{} - validators.TimeDurationString().ValidateString(context.Background(), request, &response) + validators.TimeDuration().ValidateString(context.Background(), request, &response) if diff := cmp.Diff(response.Diagnostics, test.expectedDiagnostics); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff) diff --git a/timeouts/datasource/schema.go b/timeouts/datasource/schema.go index 264576f..886a0c7 100644 --- a/timeouts/datasource/schema.go +++ b/timeouts/datasource/schema.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" ) const ( @@ -28,7 +27,7 @@ type Opts struct { func Block(ctx context.Context) schema.Block { return schema.SingleNestedBlock{ Attributes: attributesMap(), - CustomType: timeouts.TimeoutsType{ + CustomType: Type{ ObjectType: types.ObjectType{ AttrTypes: attrTypesMap(), }, @@ -43,7 +42,7 @@ func Block(ctx context.Context) schema.Block { func Attributes(ctx context.Context) schema.Attribute { return schema.SingleNestedAttribute{ Attributes: attributesMap(), - CustomType: timeouts.TimeoutsType{ + CustomType: Type{ ObjectType: types.ObjectType{ AttrTypes: attrTypesMap(), }, @@ -57,7 +56,7 @@ func attributesMap() map[string]schema.Attribute { attributeNameRead: schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, } diff --git a/timeouts/datasource/schema_test.go b/timeouts/datasource/schema_test.go index 4739505..28cd10b 100644 --- a/timeouts/datasource/schema_test.go +++ b/timeouts/datasource/schema_test.go @@ -11,8 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" - datasourcetimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" ) func TestBlock(t *testing.T) { @@ -24,7 +23,7 @@ func TestBlock(t *testing.T) { tests := map[string]testCase{ "read": { expected: schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "read": types.StringType, @@ -35,7 +34,7 @@ func TestBlock(t *testing.T) { "read": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, @@ -46,7 +45,7 @@ func TestBlock(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - actual := datasourcetimeouts.Block(context.Background()) + actual := timeouts.Block(context.Background()) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected block difference: %s", diff) @@ -68,11 +67,11 @@ func TestAttributes(t *testing.T) { "read": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "read": types.StringType, @@ -87,7 +86,7 @@ func TestAttributes(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - actual := datasourcetimeouts.Attributes(context.Background()) + actual := timeouts.Attributes(context.Background()) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected block difference: %s", diff) diff --git a/timeouts/datasource/timeouts.go b/timeouts/datasource/timeouts.go new file mode 100644 index 0000000..ccf94ae --- /dev/null +++ b/timeouts/datasource/timeouts.go @@ -0,0 +1,109 @@ +package timeouts + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +const defaultTimeout = time.Minute * 20 + +// Type is an attribute type that represents timeouts. +type Type struct { + types.ObjectType +} + +// ValueFromTerraform returns a Value given a tftypes.Value. +// Value embeds the types.Object value returned from calling ValueFromTerraform on the +// types.ObjectType embedded in Type. +func (t Type) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + val, err := t.ObjectType.ValueFromTerraform(ctx, in) + if err != nil { + return nil, err + } + + obj, ok := val.(types.Object) + if !ok { + return nil, fmt.Errorf("%T cannot be used as types.Object", val) + } + + return Value{ + obj, + }, err +} + +// Equal returns true if `candidate` is also an Type and has the same +// AttributeTypes. +func (t Type) Equal(candidate attr.Type) bool { + other, ok := candidate.(Type) + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + +// Value represents an object containing values to be used as time.Duration for timeouts. +type Value struct { + types.Object +} + +// Equal returns true if the Value is considered semantically equal +// (same type and same value) to the attr.Value passed as an argument. +func (t Value) Equal(c attr.Value) bool { + other, ok := c.(Value) + + if !ok { + return false + } + + return t.Object.Equal(other.Object) +} + +// Type returns an Type with the same attribute types as `t`. +func (t Value) Type(ctx context.Context) attr.Type { + return Type{ + types.ObjectType{ + AttrTypes: t.AttributeTypes(ctx), + }, + } +} + +// Read attempts to retrieve the "read" attribute and parse it as time.Duration. +// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. +func (t Value) Read(ctx context.Context) (time.Duration, diag.Diagnostics) { + return t.getTimeout(ctx, attributeNameRead) +} + +func (t Value) getTimeout(_ context.Context, timeoutName string) (time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + + value, ok := t.Object.Attributes()[timeoutName] + if !ok { + diags.Append(diag.NewErrorDiagnostic( + "Timeout Does Not Exist", + fmt.Sprintf("timeout for %q does not exist", timeoutName), + )) + + return defaultTimeout, diags + } + + // No type assertion check is required as the schema guarantees that the object attributes + // are types.String. + timeout, err := time.ParseDuration(value.(types.String).ValueString()) + if err != nil { + diags.Append(diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + fmt.Sprintf("timeout for %q cannot be parsed, %s", timeoutName, err), + )) + + return defaultTimeout, diags + } + + return timeout, diags +} diff --git a/timeouts/datasource/timeouts_test.go b/timeouts/datasource/timeouts_test.go new file mode 100644 index 0000000..1137064 --- /dev/null +++ b/timeouts/datasource/timeouts_test.go @@ -0,0 +1,220 @@ +package timeouts_test + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" +) + +func TestTimeoutsTypeValueFromTerraform(t *testing.T) { + t.Parallel() + + type testCase struct { + receiver timeouts.Type + input tftypes.Value + expected attr.Value + expectedErr string + } + tests := map[string]testCase{ + "basic-object": { + receiver: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + }, + input: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "read": tftypes.String, + }, + }, map[string]tftypes.Value{ + "read": tftypes.NewValue(tftypes.String, "30m"), + }), + expected: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "read": types.StringType, + }, + map[string]attr.Value{ + "read": types.StringValue("30m"), + }, + ), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := test.receiver.ValueFromTerraform(context.Background(), test.input) + if err != nil { + if test.expectedErr == "" { + t.Errorf("Unexpected error: %s", err.Error()) + return + } + if err.Error() != test.expectedErr { + t.Errorf("Expected error to be %q, got %q", test.expectedErr, err.Error()) + return + } + } + + if diff := cmp.Diff(test.expected, got); diff != "" { + t.Errorf("unexpected result (-expected, +got): %s", diff) + } + }) + } +} + +func TestTimeoutsTypeEqual(t *testing.T) { + t.Parallel() + + type testCase struct { + receiver timeouts.Type + input attr.Type + expected bool + } + tests := map[string]testCase{ + "equal": { + receiver: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "c": types.BoolType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + input: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "c": types.BoolType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + expected: true, + }, + "missing-attr": { + receiver: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "c": types.BoolType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + input: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + "a": types.StringType, + "b": types.NumberType, + "d": types.ListType{ + ElemType: types.StringType, + }, + }}}, + expected: false, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.receiver.Equal(test.input) + if test.expected != got { + t.Errorf("Expected %v, got %v", test.expected, got) + } + }) + } +} + +func TestTimeoutsValueRead(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.Value + expectedTimeout time.Duration + expectedDiags diag.Diagnostics + } + tests := map[string]testCase{ + "read": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "read": types.StringType, + }, + map[string]attr.Value{ + "read": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedDiags: nil, + }, + "read-not-set": { + timeoutsValue: timeouts.Value{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Does Not Exist", + `timeout for "read" does not exist`, + ), + }, + }, + "read-not-parseable-as-time-duration": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "read": types.StringType, + }, + map[string]attr.Value{ + "read": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + `timeout for "read" cannot be parsed, time: unknown unit "x" in duration "10x"`, + ), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + gotTimeout, gotErr := test.timeoutsValue.Read(context.Background()) + + if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { + t.Errorf("unexpected timeout difference: %s", diff) + } + + if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +// equateErrorMessage reports errors to be equal if both are nil +// or both have the same message. +var equateErrorMessage = cmp.Comparer(func(x, y error) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.Error() == y.Error() +}) diff --git a/timeouts/resource/schema.go b/timeouts/resource/schema.go index f90864d..0c0e123 100644 --- a/timeouts/resource/schema.go +++ b/timeouts/resource/schema.go @@ -9,7 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" ) const ( @@ -35,7 +34,7 @@ type Opts struct { func Block(ctx context.Context, opts Opts) schema.Block { return schema.SingleNestedBlock{ Attributes: attributesMap(opts), - CustomType: timeouts.TimeoutsType{ + CustomType: Type{ ObjectType: types.ObjectType{ AttrTypes: attrTypesMap(opts), }, @@ -63,7 +62,7 @@ func BlockAll(ctx context.Context) schema.Block { func Attributes(ctx context.Context, opts Opts) schema.Attribute { return schema.SingleNestedAttribute{ Attributes: attributesMap(opts), - CustomType: timeouts.TimeoutsType{ + CustomType: Type{ ObjectType: types.ObjectType{ AttrTypes: attrTypesMap(opts), }, @@ -90,7 +89,7 @@ func attributesMap(opts Opts) map[string]schema.Attribute { attribute := schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, } diff --git a/timeouts/resource/schema_test.go b/timeouts/resource/schema_test.go index 4802c98..f690cc5 100644 --- a/timeouts/resource/schema_test.go +++ b/timeouts/resource/schema_test.go @@ -11,22 +11,21 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" - resourcetimeouts "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" ) func TestBlock(t *testing.T) { t.Parallel() type testCase struct { - opts resourcetimeouts.Opts + opts timeouts.Opts expected schema.Block } tests := map[string]testCase{ "empty-opts": { - opts: resourcetimeouts.Opts{}, + opts: timeouts.Opts{}, expected: schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{}, }, @@ -35,11 +34,11 @@ func TestBlock(t *testing.T) { }, }, "create-opts": { - opts: resourcetimeouts.Opts{ + opts: timeouts.Opts{ Create: true, }, expected: schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "create": types.StringType, @@ -50,19 +49,19 @@ func TestBlock(t *testing.T) { "create": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, }, }, "create-update-opts": { - opts: resourcetimeouts.Opts{ + opts: timeouts.Opts{ Create: true, Update: true, }, expected: schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "create": types.StringType, @@ -74,13 +73,13 @@ func TestBlock(t *testing.T) { "create": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "update": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, @@ -91,7 +90,7 @@ func TestBlock(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - actual := resourcetimeouts.Block(context.Background(), test.opts) + actual := timeouts.Block(context.Background(), test.opts) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected block difference: %s", diff) @@ -103,10 +102,10 @@ func TestBlock(t *testing.T) { func TestBlockAll(t *testing.T) { t.Parallel() - actual := resourcetimeouts.BlockAll(context.Background()) + actual := timeouts.BlockAll(context.Background()) expected := schema.SingleNestedBlock{ - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "create": types.StringType, @@ -120,25 +119,25 @@ func TestBlockAll(t *testing.T) { "create": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "read": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "update": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "delete": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, @@ -153,15 +152,15 @@ func TestAttributes(t *testing.T) { t.Parallel() type testCase struct { - opts resourcetimeouts.Opts + opts timeouts.Opts expected schema.Attribute } tests := map[string]testCase{ "empty-opts": { - opts: resourcetimeouts.Opts{}, + opts: timeouts.Opts{}, expected: schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{}, - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{}, }, @@ -170,7 +169,7 @@ func TestAttributes(t *testing.T) { }, }, "create-opts": { - opts: resourcetimeouts.Opts{ + opts: timeouts.Opts{ Create: true, }, expected: schema.SingleNestedAttribute{ @@ -178,11 +177,11 @@ func TestAttributes(t *testing.T) { "create": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "create": types.StringType, @@ -193,7 +192,7 @@ func TestAttributes(t *testing.T) { }, }, "create-update-opts": { - opts: resourcetimeouts.Opts{ + opts: timeouts.Opts{ Create: true, Update: true, }, @@ -202,17 +201,17 @@ func TestAttributes(t *testing.T) { "create": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "update": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "create": types.StringType, @@ -228,7 +227,7 @@ func TestAttributes(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - actual := resourcetimeouts.Attributes(context.Background(), test.opts) + actual := timeouts.Attributes(context.Background(), test.opts) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected block difference: %s", diff) @@ -240,36 +239,36 @@ func TestAttributes(t *testing.T) { func TestAttributesAll(t *testing.T) { t.Parallel() - actual := resourcetimeouts.AttributesAll(context.Background()) + actual := timeouts.AttributesAll(context.Background()) expected := schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "create": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "read": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "update": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, "delete": schema.StringAttribute{ Optional: true, Validators: []validator.String{ - validators.TimeDurationString(), + validators.TimeDuration(), }, }, }, - CustomType: timeouts.TimeoutsType{ + CustomType: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "create": types.StringType, diff --git a/timeouts/timeouts.go b/timeouts/resource/timeouts.go similarity index 57% rename from timeouts/timeouts.go rename to timeouts/resource/timeouts.go index 20d7816..da4cced 100644 --- a/timeouts/timeouts.go +++ b/timeouts/resource/timeouts.go @@ -11,23 +11,17 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) -const ( - defaultTimeout = time.Minute * 20 - attributeNameCreate = "create" - attributeNameRead = "read" - attributeNameUpdate = "update" - attributeNameDelete = "delete" -) +const defaultTimeout = time.Minute * 20 -// TimeoutsType is an attribute type that represents timeouts. -type TimeoutsType struct { +// Type is an attribute type that represents timeouts. +type Type struct { types.ObjectType } -// ValueFromTerraform returns a TimeoutsValue given a tftypes.Value. -// TimeoutsValue embeds the types.Object value returned from calling ValueFromTerraform on the -// types.ObjectType embedded in TimeoutsType. -func (t TimeoutsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { +// ValueFromTerraform returns a Value given a tftypes.Value. +// Value embeds the types.Object value returned from calling ValueFromTerraform on the +// types.ObjectType embedded in Type. +func (t Type) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { val, err := t.ObjectType.ValueFromTerraform(ctx, in) if err != nil { return nil, err @@ -38,15 +32,15 @@ func (t TimeoutsType) ValueFromTerraform(ctx context.Context, in tftypes.Value) return nil, fmt.Errorf("%T cannot be used as types.Object", val) } - return TimeoutsValue{ + return Value{ obj, }, err } -// Equal returns true if `candidate` is also an TimeoutsType and has the same +// Equal returns true if `candidate` is also a Type and has the same // AttributeTypes. -func (t TimeoutsType) Equal(candidate attr.Type) bool { - other, ok := candidate.(TimeoutsType) +func (t Type) Equal(candidate attr.Type) bool { + other, ok := candidate.(Type) if !ok { return false } @@ -54,15 +48,15 @@ func (t TimeoutsType) Equal(candidate attr.Type) bool { return t.ObjectType.Equal(other.ObjectType) } -// TimeoutsValue represents an object containing values to be used as time.Duration for timeouts. -type TimeoutsValue struct { +// Value represents an object containing values to be used as time.Duration for timeouts. +type Value struct { types.Object } -// Equal returns true if the TimeoutsValue is considered semantically equal +// Equal returns true if the Value is considered semantically equal // (same type and same value) to the attr.Value passed as an argument. -func (t TimeoutsValue) Equal(c attr.Value) bool { - other, ok := c.(TimeoutsValue) +func (t Value) Equal(c attr.Value) bool { + other, ok := c.(Value) if !ok { return false @@ -71,9 +65,9 @@ func (t TimeoutsValue) Equal(c attr.Value) bool { return t.Object.Equal(other.Object) } -// Type returns an TimeoutsType with the same attribute types as `t`. -func (t TimeoutsValue) Type(ctx context.Context) attr.Type { - return TimeoutsType{ +// Type returns an Type with the same attribute types as `t`. +func (t Value) Type(ctx context.Context) attr.Type { + return Type{ types.ObjectType{ AttrTypes: t.AttributeTypes(ctx), }, @@ -82,29 +76,29 @@ func (t TimeoutsValue) Type(ctx context.Context) attr.Type { // Create attempts to retrieve the "create" attribute and parse it as time.Duration. // If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Create(ctx context.Context) (time.Duration, diag.Diagnostics) { +func (t Value) Create(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameCreate) } // Read attempts to retrieve the "read" attribute and parse it as time.Duration. // If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Read(ctx context.Context) (time.Duration, diag.Diagnostics) { +func (t Value) Read(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameRead) } // Update attempts to retrieve the "update" attribute and parse it as time.Duration. // If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Update(ctx context.Context) (time.Duration, diag.Diagnostics) { +func (t Value) Update(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameUpdate) } // Delete attempts to retrieve the "delete" attribute and parse it as time.Duration. // If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t TimeoutsValue) Delete(ctx context.Context) (time.Duration, diag.Diagnostics) { +func (t Value) Delete(ctx context.Context) (time.Duration, diag.Diagnostics) { return t.getTimeout(ctx, attributeNameDelete) } -func (t TimeoutsValue) getTimeout(_ context.Context, timeoutName string) (time.Duration, diag.Diagnostics) { +func (t Value) getTimeout(_ context.Context, timeoutName string) (time.Duration, diag.Diagnostics) { var diags diag.Diagnostics value, ok := t.Object.Attributes()[timeoutName] @@ -119,7 +113,7 @@ func (t TimeoutsValue) getTimeout(_ context.Context, timeoutName string) (time.D // No type assertion check is required as the schema guarantees that the object attributes // are types.String. - createTimeout, err := time.ParseDuration(value.(types.String).ValueString()) + timeout, err := time.ParseDuration(value.(types.String).ValueString()) if err != nil { diags.Append(diag.NewErrorDiagnostic( "Timeout Cannot Be Parsed", @@ -129,5 +123,5 @@ func (t TimeoutsValue) getTimeout(_ context.Context, timeoutName string) (time.D return defaultTimeout, diags } - return createTimeout, diags + return timeout, diags } diff --git a/timeouts/timeouts_test.go b/timeouts/resource/timeouts_test.go similarity index 89% rename from timeouts/timeouts_test.go rename to timeouts/resource/timeouts_test.go index 098aa0d..40c32fb 100644 --- a/timeouts/timeouts_test.go +++ b/timeouts/resource/timeouts_test.go @@ -11,21 +11,21 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" ) func TestTimeoutsTypeValueFromTerraform(t *testing.T) { t.Parallel() type testCase struct { - receiver timeouts.TimeoutsType + receiver timeouts.Type input tftypes.Value expected attr.Value expectedErr string } tests := map[string]testCase{ "basic-object": { - receiver: timeouts.TimeoutsType{ + receiver: timeouts.Type{ ObjectType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "create": types.StringType, @@ -48,7 +48,7 @@ func TestTimeoutsTypeValueFromTerraform(t *testing.T) { "update": tftypes.NewValue(tftypes.String, "10m"), "delete": tftypes.NewValue(tftypes.String, "25m"), }), - expected: timeouts.TimeoutsValue{ + expected: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "create": types.StringType, @@ -95,13 +95,13 @@ func TestTimeoutsTypeEqual(t *testing.T) { t.Parallel() type testCase struct { - receiver timeouts.TimeoutsType + receiver timeouts.Type input attr.Type expected bool } tests := map[string]testCase{ "equal": { - receiver: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + receiver: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ "a": types.StringType, "b": types.NumberType, "c": types.BoolType, @@ -109,7 +109,7 @@ func TestTimeoutsTypeEqual(t *testing.T) { ElemType: types.StringType, }, }}}, - input: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + input: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ "a": types.StringType, "b": types.NumberType, "c": types.BoolType, @@ -120,7 +120,7 @@ func TestTimeoutsTypeEqual(t *testing.T) { expected: true, }, "missing-attr": { - receiver: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + receiver: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ "a": types.StringType, "b": types.NumberType, "c": types.BoolType, @@ -128,7 +128,7 @@ func TestTimeoutsTypeEqual(t *testing.T) { ElemType: types.StringType, }, }}}, - input: timeouts.TimeoutsType{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ + input: timeouts.Type{ObjectType: types.ObjectType{AttrTypes: map[string]attr.Type{ "a": types.StringType, "b": types.NumberType, "d": types.ListType{ @@ -155,13 +155,13 @@ func TestTimeoutsValueCreate(t *testing.T) { t.Parallel() type testCase struct { - timeoutsValue timeouts.TimeoutsValue + timeoutsValue timeouts.Value expectedTimeout time.Duration expectedDiags diag.Diagnostics } tests := map[string]testCase{ "create": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "create": types.StringType, @@ -175,7 +175,7 @@ func TestTimeoutsValueCreate(t *testing.T) { expectedDiags: nil, }, "create-not-set": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, @@ -187,7 +187,7 @@ func TestTimeoutsValueCreate(t *testing.T) { }, }, "create-not-parseable-as-time-duration": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "create": types.StringType, @@ -229,13 +229,13 @@ func TestTimeoutsValueRead(t *testing.T) { t.Parallel() type testCase struct { - timeoutsValue timeouts.TimeoutsValue + timeoutsValue timeouts.Value expectedTimeout time.Duration expectedDiags diag.Diagnostics } tests := map[string]testCase{ "read": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "read": types.StringType, @@ -249,7 +249,7 @@ func TestTimeoutsValueRead(t *testing.T) { expectedDiags: nil, }, "read-not-set": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, @@ -261,7 +261,7 @@ func TestTimeoutsValueRead(t *testing.T) { }, }, "read-not-parseable-as-time-duration": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "read": types.StringType, @@ -303,13 +303,13 @@ func TestTimeoutsValueUpdate(t *testing.T) { t.Parallel() type testCase struct { - timeoutsValue timeouts.TimeoutsValue + timeoutsValue timeouts.Value expectedTimeout time.Duration expectedDiags diag.Diagnostics } tests := map[string]testCase{ "update": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "update": types.StringType, @@ -323,7 +323,7 @@ func TestTimeoutsValueUpdate(t *testing.T) { expectedDiags: nil, }, "update-not-set": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, @@ -335,7 +335,7 @@ func TestTimeoutsValueUpdate(t *testing.T) { }, }, "update-not-parseable-as-time-duration": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "update": types.StringType, @@ -377,13 +377,13 @@ func TestTimeoutsValueDelete(t *testing.T) { t.Parallel() type testCase struct { - timeoutsValue timeouts.TimeoutsValue + timeoutsValue timeouts.Value expectedTimeout time.Duration expectedDiags diag.Diagnostics } tests := map[string]testCase{ "delete": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "delete": types.StringType, @@ -397,7 +397,7 @@ func TestTimeoutsValueDelete(t *testing.T) { expectedDiags: nil, }, "delete-not-set": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, @@ -409,7 +409,7 @@ func TestTimeoutsValueDelete(t *testing.T) { }, }, "delete-not-parseable-as-time-duration": { - timeoutsValue: timeouts.TimeoutsValue{ + timeoutsValue: timeouts.Value{ Object: types.ObjectValueMust( map[string]attr.Type{ "delete": types.StringType, From 1459b162d1aca6a99685978413bb6ee232c0ad7e Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Dec 2022 07:51:28 +0000 Subject: [PATCH 13/17] Bump terraform-plugin-framework to v1.0.0 (#17) --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index a3df0f7..fa7255b 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,14 @@ go 1.18 require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/terraform-plugin-framework v0.17.0 + github.com/hashicorp/terraform-plugin-framework v1.0.0 + github.com/hashicorp/terraform-plugin-go v0.14.2 ) require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/terraform-plugin-go v0.14.2 // indirect github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index 5b2446a..80429aa 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/terraform-plugin-framework v0.17.0 h1:0KUOY/oe1GPLFqaXnKDnd1rhCrnUtt8pV9wGEwNUFlU= -github.com/hashicorp/terraform-plugin-framework v0.17.0/go.mod h1:FV97t2BZOARkL7NNlsc/N25c84MyeSSz72uPp7Vq1lg= +github.com/hashicorp/terraform-plugin-framework v1.0.0 h1:0Mls4TrMTrDysBUby/UmlbcTOMM+n5JBDyB5k+XkGWg= +github.com/hashicorp/terraform-plugin-framework v1.0.0/go.mod h1:FV97t2BZOARkL7NNlsc/N25c84MyeSSz72uPp7Vq1lg= github.com/hashicorp/terraform-plugin-go v0.14.2 h1:rhsVEOGCnY04msNymSvbUsXfRLKh9znXZmHlf5e8mhE= github.com/hashicorp/terraform-plugin-go v0.14.2/go.mod h1:Q12UjumPNGiFsZffxOsA40Tlz1WVXt2Evh865Zj0+UA= github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs= From 1dc5f00ff0d7e5dd914adc71f3b201fcd17833f8 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Fri, 16 Dec 2022 08:22:23 +0000 Subject: [PATCH 14/17] Restructuring packages (#17) --- .changelog/18.txt | 8 ++------ README.md | 8 ++++---- {timeouts/datasource => datasource/timeouts}/schema.go | 6 ------ .../datasource => datasource/timeouts}/schema_test.go | 2 +- {timeouts/datasource => datasource/timeouts}/timeouts.go | 0 .../datasource => datasource/timeouts}/timeouts_test.go | 2 +- {timeouts/resource => resource/timeouts}/schema.go | 0 {timeouts/resource => resource/timeouts}/schema_test.go | 2 +- {timeouts/resource => resource/timeouts}/timeouts.go | 0 {timeouts/resource => resource/timeouts}/timeouts_test.go | 2 +- 10 files changed, 10 insertions(+), 20 deletions(-) rename {timeouts/datasource => datasource/timeouts}/schema.go (92%) rename {timeouts/datasource => datasource/timeouts}/schema_test.go (96%) rename {timeouts/datasource => datasource/timeouts}/timeouts.go (100%) rename {timeouts/datasource => datasource/timeouts}/timeouts_test.go (98%) rename {timeouts/resource => resource/timeouts}/schema.go (100%) rename {timeouts/resource => resource/timeouts}/schema_test.go (98%) rename {timeouts/resource => resource/timeouts}/timeouts.go (100%) rename {timeouts/resource => resource/timeouts}/timeouts_test.go (99%) diff --git a/.changelog/18.txt b/.changelog/18.txt index 730b686..e1299a4 100644 --- a/.changelog/18.txt +++ b/.changelog/18.txt @@ -1,13 +1,9 @@ ```release-note:feature -Introduced datasource `timeouts` package for use with datasource schema +Introduced `datasourcetimeouts` package for use with datasource schema ``` ```release-note:feature -Introduced resource `timeouts` package for use with resource schema -``` - -```release-note:feature -Introduced type `timeouts` package to provider `TimeoutsType` and `TimeoutsValue` for use with schema and data models, respectively +Introduced `resourcetimeouts` package for use with resource schema ``` ```release-note:breaking-change diff --git a/README.md b/README.md index fb230a9..242806f 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ You must supply `timeouts.Opts` when calling `timeouts.Block()` on a resource. A ```go import ( /* ... */ - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" ) func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -86,7 +86,7 @@ The `timeouts.Block()` call does not accept options on a data source as `read` i ```go import ( /* ... */ - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" ) func (t exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { @@ -131,7 +131,7 @@ You must supply `timeouts.Opts` when calling `timeouts.Attributes()` on a resour ```go import ( /* ... */ - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" ) func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -149,7 +149,7 @@ The `timeouts.Attributes()` call does not accept options on a data source as `re ```go import ( /* ... */ - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" ) func (t exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { diff --git a/timeouts/datasource/schema.go b/datasource/timeouts/schema.go similarity index 92% rename from timeouts/datasource/schema.go rename to datasource/timeouts/schema.go index 886a0c7..52938ee 100644 --- a/timeouts/datasource/schema.go +++ b/datasource/timeouts/schema.go @@ -15,12 +15,6 @@ const ( attributeNameRead = "read" ) -// Opts is used as an argument to Block and Attributes to indicate which attributes -// should be created. -type Opts struct { - Read bool -} - // Block returns a schema.Block containing attributes for `Read`, which is // defined as types.StringType and optional. A validator is used to verify // that the value assigned to `Read` can be parsed as time.Duration. diff --git a/timeouts/datasource/schema_test.go b/datasource/timeouts/schema_test.go similarity index 96% rename from timeouts/datasource/schema_test.go rename to datasource/timeouts/schema_test.go index 28cd10b..195a8ba 100644 --- a/timeouts/datasource/schema_test.go +++ b/datasource/timeouts/schema_test.go @@ -10,8 +10,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/datasource" ) func TestBlock(t *testing.T) { diff --git a/timeouts/datasource/timeouts.go b/datasource/timeouts/timeouts.go similarity index 100% rename from timeouts/datasource/timeouts.go rename to datasource/timeouts/timeouts.go diff --git a/timeouts/datasource/timeouts_test.go b/datasource/timeouts/timeouts_test.go similarity index 98% rename from timeouts/datasource/timeouts_test.go rename to datasource/timeouts/timeouts_test.go index 1137064..4b764b1 100644 --- a/timeouts/datasource/timeouts_test.go +++ b/datasource/timeouts/timeouts_test.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" ) func TestTimeoutsTypeValueFromTerraform(t *testing.T) { diff --git a/timeouts/resource/schema.go b/resource/timeouts/schema.go similarity index 100% rename from timeouts/resource/schema.go rename to resource/timeouts/schema.go diff --git a/timeouts/resource/schema_test.go b/resource/timeouts/schema_test.go similarity index 98% rename from timeouts/resource/schema_test.go rename to resource/timeouts/schema_test.go index f690cc5..944c542 100644 --- a/timeouts/resource/schema_test.go +++ b/resource/timeouts/schema_test.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" ) func TestBlock(t *testing.T) { diff --git a/timeouts/resource/timeouts.go b/resource/timeouts/timeouts.go similarity index 100% rename from timeouts/resource/timeouts.go rename to resource/timeouts/timeouts.go diff --git a/timeouts/resource/timeouts_test.go b/resource/timeouts/timeouts_test.go similarity index 99% rename from timeouts/resource/timeouts_test.go rename to resource/timeouts/timeouts_test.go index 40c32fb..4fcd3e3 100644 --- a/timeouts/resource/timeouts_test.go +++ b/resource/timeouts/timeouts_test.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-framework-timeouts/timeouts/resource" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" ) func TestTimeoutsTypeValueFromTerraform(t *testing.T) { From ea45484f39aef533ea2974184b9c03260e0e1fd2 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 20 Dec 2022 16:39:40 +0000 Subject: [PATCH 15/17] Apply suggestions from code review Co-authored-by: Brian Flad --- .changelog/18.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changelog/18.txt b/.changelog/18.txt index e1299a4..6dddec1 100644 --- a/.changelog/18.txt +++ b/.changelog/18.txt @@ -1,9 +1,9 @@ ```release-note:feature -Introduced `datasourcetimeouts` package for use with datasource schema +Introduced `datasource/timeouts` package for use with datasource schema ``` ```release-note:feature -Introduced `resourcetimeouts` package for use with resource schema +Introduced `resource/timeouts` package for use with resource schema ``` ```release-note:breaking-change From 61c354d0da6a403e9050aa4b33c308747ff67ad2 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 20 Dec 2022 16:43:20 +0000 Subject: [PATCH 16/17] Refactoring to use a supplied default timeout if the requested timeout is not in the Terraform configuration (#17) --- README.md | 14 ++++---- datasource/timeouts/timeouts.go | 18 ++++------ datasource/timeouts/timeouts_test.go | 19 ++--------- resource/timeouts/timeouts.go | 36 +++++++++----------- resource/timeouts/timeouts_test.go | 49 +++++----------------------- 5 files changed, 41 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 242806f..5c10388 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Terraform configuration. The following illustrates nested block syntax for defining timeouts on a resource and a data source. -```hcl +```terraform resource "timeouts_example" "example" { /* ... */ @@ -48,7 +48,7 @@ resource "timeouts_example" "example" { } ``` -```hcl +```terraform data "timeouts_example" "example" { /* ... */ @@ -104,7 +104,7 @@ func (t exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequ The following illustrates nested attribute syntax for defining timeouts on a resource and a data source. -```hcl +```terraform resource "timeouts_example" "example" { /* ... */ @@ -114,7 +114,7 @@ resource "timeouts_example" "example" { } ``` -```hcl +```terraform data "timeouts_example" "example" { /* ... */ @@ -197,8 +197,10 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, if resp.Diagnostics.HasError() { return } - - createTimeout, err := data.Timeouts.Create(ctx) + + // Create() is passed a default timeout to use if no value + // has been supplied in the Terraform configuration. + createTimeout, err := data.Timeouts.Create(ctx, 20*time.Minute) if err != nil { // handle error } diff --git a/datasource/timeouts/timeouts.go b/datasource/timeouts/timeouts.go index ccf94ae..21f1caa 100644 --- a/datasource/timeouts/timeouts.go +++ b/datasource/timeouts/timeouts.go @@ -9,10 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tflog" ) -const defaultTimeout = time.Minute * 20 - // Type is an attribute type that represents timeouts. type Type struct { types.ObjectType @@ -65,7 +64,7 @@ func (t Value) Equal(c attr.Value) bool { return t.Object.Equal(other.Object) } -// Type returns an Type with the same attribute types as `t`. +// Type returns a Type with the same attribute types as `t`. func (t Value) Type(ctx context.Context) attr.Type { return Type{ types.ObjectType{ @@ -75,20 +74,17 @@ func (t Value) Type(ctx context.Context) attr.Type { } // Read attempts to retrieve the "read" attribute and parse it as time.Duration. -// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t Value) Read(ctx context.Context) (time.Duration, diag.Diagnostics) { - return t.getTimeout(ctx, attributeNameRead) +// If any diagnostics are generated they are returned along with the supplied default timeout. +func (t Value) Read(ctx context.Context, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { + return t.getTimeout(ctx, attributeNameRead, defaultTimeout) } -func (t Value) getTimeout(_ context.Context, timeoutName string) (time.Duration, diag.Diagnostics) { +func (t Value) getTimeout(ctx context.Context, timeoutName string, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { var diags diag.Diagnostics value, ok := t.Object.Attributes()[timeoutName] if !ok { - diags.Append(diag.NewErrorDiagnostic( - "Timeout Does Not Exist", - fmt.Sprintf("timeout for %q does not exist", timeoutName), - )) + tflog.Info(ctx, timeoutName+" timeout configuration not found, using provided default") return defaultTimeout, diags } diff --git a/datasource/timeouts/timeouts_test.go b/datasource/timeouts/timeouts_test.go index 4b764b1..ae683e7 100644 --- a/datasource/timeouts/timeouts_test.go +++ b/datasource/timeouts/timeouts_test.go @@ -164,12 +164,6 @@ func TestTimeoutsValueRead(t *testing.T) { Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedDiags: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Timeout Does Not Exist", - `timeout for "read" does not exist`, - ), - }, }, "read-not-parseable-as-time-duration": { timeoutsValue: timeouts.Value{ @@ -197,24 +191,15 @@ func TestTimeoutsValueRead(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - gotTimeout, gotErr := test.timeoutsValue.Read(context.Background()) + gotTimeout, gotErr := test.timeoutsValue.Read(context.Background(), 20*time.Minute) if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) } } - -// equateErrorMessage reports errors to be equal if both are nil -// or both have the same message. -var equateErrorMessage = cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() -}) diff --git a/resource/timeouts/timeouts.go b/resource/timeouts/timeouts.go index da4cced..d2b7cfa 100644 --- a/resource/timeouts/timeouts.go +++ b/resource/timeouts/timeouts.go @@ -9,10 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tflog" ) -const defaultTimeout = time.Minute * 20 - // Type is an attribute type that represents timeouts. type Type struct { types.ObjectType @@ -65,7 +64,7 @@ func (t Value) Equal(c attr.Value) bool { return t.Object.Equal(other.Object) } -// Type returns an Type with the same attribute types as `t`. +// Type returns a Type with the same attribute types as `t`. func (t Value) Type(ctx context.Context) attr.Type { return Type{ types.ObjectType{ @@ -75,38 +74,35 @@ func (t Value) Type(ctx context.Context) attr.Type { } // Create attempts to retrieve the "create" attribute and parse it as time.Duration. -// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t Value) Create(ctx context.Context) (time.Duration, diag.Diagnostics) { - return t.getTimeout(ctx, attributeNameCreate) +// If any diagnostics are generated they are returned along with the supplied default timeout. +func (t Value) Create(ctx context.Context, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { + return t.getTimeout(ctx, attributeNameCreate, defaultTimeout) } // Read attempts to retrieve the "read" attribute and parse it as time.Duration. -// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t Value) Read(ctx context.Context) (time.Duration, diag.Diagnostics) { - return t.getTimeout(ctx, attributeNameRead) +// If any diagnostics are generated they are returned along with the supplied default timeout. +func (t Value) Read(ctx context.Context, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { + return t.getTimeout(ctx, attributeNameRead, defaultTimeout) } // Update attempts to retrieve the "update" attribute and parse it as time.Duration. -// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t Value) Update(ctx context.Context) (time.Duration, diag.Diagnostics) { - return t.getTimeout(ctx, attributeNameUpdate) +// If any diagnostics are generated they are returned along with the supplied default timeout. +func (t Value) Update(ctx context.Context, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { + return t.getTimeout(ctx, attributeNameUpdate, defaultTimeout) } // Delete attempts to retrieve the "delete" attribute and parse it as time.Duration. -// If any diagnostics are generated they are returned along with the default timeout of 20 minutes. -func (t Value) Delete(ctx context.Context) (time.Duration, diag.Diagnostics) { - return t.getTimeout(ctx, attributeNameDelete) +// If any diagnostics are generated they are returned along with the supplied default timeout. +func (t Value) Delete(ctx context.Context, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { + return t.getTimeout(ctx, attributeNameDelete, defaultTimeout) } -func (t Value) getTimeout(_ context.Context, timeoutName string) (time.Duration, diag.Diagnostics) { +func (t Value) getTimeout(ctx context.Context, timeoutName string, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { var diags diag.Diagnostics value, ok := t.Object.Attributes()[timeoutName] if !ok { - diags.Append(diag.NewErrorDiagnostic( - "Timeout Does Not Exist", - fmt.Sprintf("timeout for %q does not exist", timeoutName), - )) + tflog.Info(ctx, timeoutName+" timeout configuration not found, using provided default") return defaultTimeout, diags } diff --git a/resource/timeouts/timeouts_test.go b/resource/timeouts/timeouts_test.go index 4fcd3e3..f2b5d51 100644 --- a/resource/timeouts/timeouts_test.go +++ b/resource/timeouts/timeouts_test.go @@ -179,12 +179,6 @@ func TestTimeoutsValueCreate(t *testing.T) { Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedDiags: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Timeout Does Not Exist", - `timeout for "create" does not exist`, - ), - }, }, "create-not-parseable-as-time-duration": { timeoutsValue: timeouts.Value{ @@ -212,13 +206,13 @@ func TestTimeoutsValueCreate(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - gotTimeout, gotErr := test.timeoutsValue.Create(context.Background()) + gotTimeout, gotErr := test.timeoutsValue.Create(context.Background(), 20*time.Minute) if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) @@ -253,12 +247,6 @@ func TestTimeoutsValueRead(t *testing.T) { Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedDiags: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Timeout Does Not Exist", - `timeout for "read" does not exist`, - ), - }, }, "read-not-parseable-as-time-duration": { timeoutsValue: timeouts.Value{ @@ -286,13 +274,13 @@ func TestTimeoutsValueRead(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - gotTimeout, gotErr := test.timeoutsValue.Read(context.Background()) + gotTimeout, gotErr := test.timeoutsValue.Read(context.Background(), 20*time.Minute) if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) @@ -327,12 +315,6 @@ func TestTimeoutsValueUpdate(t *testing.T) { Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedDiags: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Timeout Does Not Exist", - `timeout for "update" does not exist`, - ), - }, }, "update-not-parseable-as-time-duration": { timeoutsValue: timeouts.Value{ @@ -360,13 +342,13 @@ func TestTimeoutsValueUpdate(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - gotTimeout, gotErr := test.timeoutsValue.Update(context.Background()) + gotTimeout, gotErr := test.timeoutsValue.Update(context.Background(), 20*time.Minute) if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) @@ -401,12 +383,6 @@ func TestTimeoutsValueDelete(t *testing.T) { Object: types.Object{}, }, expectedTimeout: 20 * time.Minute, - expectedDiags: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Timeout Does Not Exist", - `timeout for "delete" does not exist`, - ), - }, }, "delete-not-parseable-as-time-duration": { timeoutsValue: timeouts.Value{ @@ -434,24 +410,15 @@ func TestTimeoutsValueDelete(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - gotTimeout, gotErr := test.timeoutsValue.Delete(context.Background()) + gotTimeout, gotErr := test.timeoutsValue.Delete(context.Background(), 20*time.Minute) if diff := cmp.Diff(gotTimeout, test.expectedTimeout); diff != "" { t.Errorf("unexpected timeout difference: %s", diff) } - if diff := cmp.Diff(gotErr, test.expectedDiags, equateErrorMessage); diff != "" { + if diff := cmp.Diff(gotErr, test.expectedDiags); diff != "" { t.Errorf("unexpected err difference: %s", diff) } }) } } - -// equateErrorMessage reports errors to be equal if both are nil -// or both have the same message. -var equateErrorMessage = cmp.Comparer(func(x, y error) bool { - if x == nil || y == nil { - return x == nil && y == nil - } - return x.Error() == y.Error() -}) From 6058ceba345cd49e7a9ab9bb3f71a13c5ba3938d Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 20 Dec 2022 16:58:14 +0000 Subject: [PATCH 17/17] Bumping terraform-plugin-framework to v1.0.1 (#17) --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index fa7255b..c04d16d 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,15 @@ go 1.18 require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/terraform-plugin-framework v1.0.0 + github.com/hashicorp/terraform-plugin-framework v1.0.1 github.com/hashicorp/terraform-plugin-go v0.14.2 + github.com/hashicorp/terraform-plugin-log v0.7.0 ) require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/go-hclog v1.2.1 // indirect - github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect diff --git a/go.sum b/go.sum index 80429aa..ca25d8a 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/terraform-plugin-framework v1.0.0 h1:0Mls4TrMTrDysBUby/UmlbcTOMM+n5JBDyB5k+XkGWg= -github.com/hashicorp/terraform-plugin-framework v1.0.0/go.mod h1:FV97t2BZOARkL7NNlsc/N25c84MyeSSz72uPp7Vq1lg= +github.com/hashicorp/terraform-plugin-framework v1.0.1 h1:apX2jtaEKa15+do6H2izBJdl1dEH2w5BPVkDJ3Q3mKA= +github.com/hashicorp/terraform-plugin-framework v1.0.1/go.mod h1:FV97t2BZOARkL7NNlsc/N25c84MyeSSz72uPp7Vq1lg= github.com/hashicorp/terraform-plugin-go v0.14.2 h1:rhsVEOGCnY04msNymSvbUsXfRLKh9znXZmHlf5e8mhE= github.com/hashicorp/terraform-plugin-go v0.14.2/go.mod h1:Q12UjumPNGiFsZffxOsA40Tlz1WVXt2Evh865Zj0+UA= github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs=