diff --git a/.changelog/18.txt b/.changelog/18.txt new file mode 100644 index 0000000..6dddec1 --- /dev/null +++ b/.changelog/18.txt @@ -0,0 +1,23 @@ +```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: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/README.md b/README.md index 59e9ad5..5c10388 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ 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 resource "timeouts_example" "example" { @@ -48,23 +48,61 @@ resource "timeouts_example" "example" { } ``` -You can use this module to mutate the `tfsdk.Schema` as follows: +```terraform +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/resource/timeouts" +) + +func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + /* ... */ + + Blocks: map[string]schema.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 -func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ +import ( + /* ... */ + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" +) + +func (t exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ /* ... */ - Blocks: map[string]tfsdk.Block{ - "timeouts": timeouts.Block(ctx, timeouts.Opts{ - Create: true, - }), + 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 resource "timeouts_example" "example" { @@ -76,12 +114,29 @@ resource "timeouts_example" "example" { } ``` -You can use this module to mutate the `tfsdk.Schema` as follows: +```terraform +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 -func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfsdk.Schema{ - Attributes: map[string]tfsdk.Attribute{ +import ( + /* ... */ + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" +) + +func (t *exampleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ /* ... */ "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ Create: true, @@ -89,25 +144,43 @@ func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Dia }, ``` +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/datasource/timeouts" +) + +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 `types.Object`. For example: +timeouts which is of type `timeouts.Value`. For example: ```go type exampleResourceData struct { /* ... */ - Timeouts types.Object `tfsdk:"timeouts"` + Timeouts timeouts.Value `tfsdk:"timeouts"` ``` ### Accessing Timeouts in CRUD Functions @@ -124,14 +197,17 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, if resp.Diagnostics.HasError() { return } - - defaultCreateTimeout := 20 * time.Minutes - createTimeout := timeouts.Create(ctx, data.Timeouts, defaultCreateTimeout) - + // 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 + } + ctx, cancel := context.WithTimeout(ctx, createTimeout) defer cancel() - + /* ... */ } ``` diff --git a/datasource/timeouts/schema.go b/datasource/timeouts/schema.go new file mode 100644 index 0000000..52938ee --- /dev/null +++ b/datasource/timeouts/schema.go @@ -0,0 +1,63 @@ +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" +) + +const ( + attributeNameRead = "read" +) + +// 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(), + CustomType: Type{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(), + }, + }, + } +} + +// 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(), + CustomType: Type{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(), + }, + }, + Optional: true, + } +} + +func attributesMap() map[string]schema.Attribute { + return map[string]schema.Attribute{ + attributeNameRead: schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + } +} + +func attrTypesMap() map[string]attr.Type { + return map[string]attr.Type{ + attributeNameRead: types.StringType, + } +} diff --git a/datasource/timeouts/schema_test.go b/datasource/timeouts/schema_test.go new file mode 100644 index 0000000..195a8ba --- /dev/null +++ b/datasource/timeouts/schema_test.go @@ -0,0 +1,96 @@ +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/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" +) + +func TestBlock(t *testing.T) { + t.Parallel() + + type testCase struct { + expected schema.Block + } + tests := map[string]testCase{ + "read": { + expected: schema.SingleNestedBlock{ + CustomType: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + }, + Attributes: map[string]schema.Attribute{ + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := timeouts.Block(context.Background()) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } + }) + } +} + +func TestAttributes(t *testing.T) { + t.Parallel() + + type testCase struct { + expected schema.Attribute + } + tests := map[string]testCase{ + "read": { + expected: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + CustomType: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + }, + Optional: true, + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := timeouts.Attributes(context.Background()) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } + }) + } +} diff --git a/datasource/timeouts/timeouts.go b/datasource/timeouts/timeouts.go new file mode 100644 index 0000000..21f1caa --- /dev/null +++ b/datasource/timeouts/timeouts.go @@ -0,0 +1,105 @@ +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" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// 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 a 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 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(ctx context.Context, timeoutName string, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + + value, ok := t.Object.Attributes()[timeoutName] + if !ok { + tflog.Info(ctx, timeoutName+" timeout configuration not found, using provided default") + + 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/datasource/timeouts/timeouts_test.go b/datasource/timeouts/timeouts_test.go new file mode 100644 index 0000000..ae683e7 --- /dev/null +++ b/datasource/timeouts/timeouts_test.go @@ -0,0 +1,205 @@ +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/datasource/timeouts" +) + +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, + }, + "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(), 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); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} diff --git a/go.mod b/go.mod index 48d19cc..c04d16d 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 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-go v0.14.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 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..ca25d8a 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 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= 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= diff --git a/internal/validators/timeduration.go b/internal/validators/timeduration.go index 9de04bc..c00b772 100644 --- a/internal/validators/timeduration.go +++ b/internal/validators/timeduration.go @@ -6,11 +6,10 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" ) -var _ tfsdk.AttributeValidator = timeDurationValidator{} +var _ validator.String = timeDurationValidator{} // timeDurationValidator validates that a string Attribute's value is parseable as time.Duration. type timeDurationValidator struct { @@ -26,17 +25,17 @@ func (validator timeDurationValidator) MarkdownDescription(ctx context.Context) return validator.Description(ctx) } -// Validate performs the validation. -func (validator timeDurationValidator) Validate(ctx context.Context, request tfsdk.ValidateAttributeRequest, response *tfsdk.ValidateAttributeResponse) { - s := request.AttributeConfig.(types.String) +// 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 { - response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( - request.AttributePath, + resp.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + req.Path, "Invalid Attribute Value Time Duration", fmt.Sprintf("%q %s", s.ValueString(), validator.Description(ctx))), ) @@ -50,6 +49,6 @@ func (validator timeDurationValidator) Validate(ctx context.Context, request tfs // - Is parseable as time duration. // // Null (unconfigured) and unknown (known after apply) values are skipped. -func TimeDuration() tfsdk.AttributeValidator { +func TimeDuration() validator.String { return timeDurationValidator{} } diff --git a/internal/validators/timeduration_test.go b/internal/validators/timeduration_test.go index cb43566..5d629aa 100644 --- a/internal/validators/timeduration_test.go +++ b/internal/validators/timeduration_test.go @@ -7,7 +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/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" @@ -46,15 +46,15 @@ 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, + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, } - response := tfsdk.ValidateAttributeResponse{} + response := validator.StringResponse{} - validators.TimeDuration().Validate(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/resource/timeouts/schema.go b/resource/timeouts/schema.go new file mode 100644 index 0000000..0c0e123 --- /dev/null +++ b/resource/timeouts/schema.go @@ -0,0 +1,135 @@ +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" +) + +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), + CustomType: Type{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(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{ + Attributes: attributesMap(opts), + CustomType: Type{ + ObjectType: types.ObjectType{ + AttrTypes: attrTypesMap(opts), + }, + }, + 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.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 +} + +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/resource/timeouts/schema_test.go b/resource/timeouts/schema_test.go new file mode 100644 index 0000000..944c542 --- /dev/null +++ b/resource/timeouts/schema_test.go @@ -0,0 +1,287 @@ +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/resource/timeouts" +) + +func TestBlock(t *testing.T) { + t.Parallel() + + type testCase struct { + opts timeouts.Opts + expected schema.Block + } + tests := map[string]testCase{ + "empty-opts": { + opts: timeouts.Opts{}, + expected: schema.SingleNestedBlock{ + CustomType: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{}, + }, + }, + Attributes: map[string]schema.Attribute{}, + }, + }, + "create-opts": { + opts: timeouts.Opts{ + Create: true, + }, + expected: schema.SingleNestedBlock{ + CustomType: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + }, + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + }, + }, + "create-update-opts": { + opts: timeouts.Opts{ + Create: true, + Update: true, + }, + expected: schema.SingleNestedBlock{ + CustomType: timeouts.Type{ + 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.TimeDuration(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + }, + }, + } + + 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) + } + }) + } +} + +func TestBlockAll(t *testing.T) { + t.Parallel() + + actual := timeouts.BlockAll(context.Background()) + + expected := schema.SingleNestedBlock{ + CustomType: timeouts.Type{ + 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.TimeDuration(), + }, + }, + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + "delete": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + } + + 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 timeouts.Opts + expected schema.Attribute + } + tests := map[string]testCase{ + "empty-opts": { + opts: timeouts.Opts{}, + expected: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{}, + CustomType: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{}, + }, + }, + Optional: true, + }, + }, + "create-opts": { + opts: timeouts.Opts{ + Create: true, + }, + expected: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + CustomType: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + }, + Optional: true, + }, + }, + "create-update-opts": { + opts: timeouts.Opts{ + Create: true, + Update: true, + }, + expected: schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + CustomType: timeouts.Type{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + "update": types.StringType, + }, + }, + }, + Optional: true, + }, + }, + } + + 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) + } + }) + } +} + +func TestAttributesAll(t *testing.T) { + t.Parallel() + + actual := timeouts.AttributesAll(context.Background()) + + expected := schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "create": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + "read": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + "update": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + "delete": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validators.TimeDuration(), + }, + }, + }, + CustomType: timeouts.Type{ + 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) + } +} diff --git a/resource/timeouts/timeouts.go b/resource/timeouts/timeouts.go new file mode 100644 index 0000000..d2b7cfa --- /dev/null +++ b/resource/timeouts/timeouts.go @@ -0,0 +1,123 @@ +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" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// 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 a 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 a 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), + }, + } +} + +// Create attempts to retrieve the "create" attribute and parse it as time.Duration. +// 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 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 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 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(ctx context.Context, timeoutName string, defaultTimeout time.Duration) (time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + + value, ok := t.Object.Attributes()[timeoutName] + if !ok { + tflog.Info(ctx, timeoutName+" timeout configuration not found, using provided default") + + 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/resource/timeouts/timeouts_test.go b/resource/timeouts/timeouts_test.go new file mode 100644 index 0000000..f2b5d51 --- /dev/null +++ b/resource/timeouts/timeouts_test.go @@ -0,0 +1,424 @@ +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/resource/timeouts" +) + +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{ + "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.Value{ + 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.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 TestTimeoutsValueCreate(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.Value + expectedTimeout time.Duration + expectedDiags diag.Diagnostics + } + tests := map[string]testCase{ + "create": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "create": types.StringType, + }, + map[string]attr.Value{ + "create": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedDiags: nil, + }, + "create-not-set": { + timeoutsValue: timeouts.Value{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + }, + "create-not-parseable-as-time-duration": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "create": types.StringType, + }, + map[string]attr.Value{ + "create": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + `timeout for "create" 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.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); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +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, + }, + "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(), 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); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +func TestTimeoutsValueUpdate(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.Value + expectedTimeout time.Duration + expectedDiags diag.Diagnostics + } + tests := map[string]testCase{ + "update": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "update": types.StringType, + }, + map[string]attr.Value{ + "update": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedDiags: nil, + }, + "update-not-set": { + timeoutsValue: timeouts.Value{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + }, + "update-not-parseable-as-time-duration": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "update": types.StringType, + }, + map[string]attr.Value{ + "update": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + `timeout for "update" 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.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); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} + +func TestTimeoutsValueDelete(t *testing.T) { + t.Parallel() + + type testCase struct { + timeoutsValue timeouts.Value + expectedTimeout time.Duration + expectedDiags diag.Diagnostics + } + tests := map[string]testCase{ + "delete": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "delete": types.StringType, + }, + map[string]attr.Value{ + "delete": types.StringValue("10m"), + }, + ), + }, + expectedTimeout: 10 * time.Minute, + expectedDiags: nil, + }, + "delete-not-set": { + timeoutsValue: timeouts.Value{ + Object: types.Object{}, + }, + expectedTimeout: 20 * time.Minute, + }, + "delete-not-parseable-as-time-duration": { + timeoutsValue: timeouts.Value{ + Object: types.ObjectValueMust( + map[string]attr.Type{ + "delete": types.StringType, + }, + map[string]attr.Value{ + "delete": types.StringValue("10x"), + }, + ), + }, + expectedTimeout: 20 * time.Minute, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Timeout Cannot Be Parsed", + `timeout for "delete" 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.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); diff != "" { + t.Errorf("unexpected err difference: %s", diff) + } + }) + } +} 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 bfccf25..0000000 --- a/timeouts/schema.go +++ /dev/null @@ -1,103 +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" -) - -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 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. -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. -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. -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. -func AttributesAll(ctx context.Context) tfsdk.Attribute { - return Attributes(ctx, Opts{ - Create: true, - Read: true, - Update: true, - Delete: true, - }) -} - -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 af10dc5..0000000 --- a/timeouts/schema_test.go +++ /dev/null @@ -1,243 +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" -) - -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) - } - }) - } -} - -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) - } -} - -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) - } - }) - } -} - -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) - } -}