From ab195cd3730ac00b1af3a7b5849079220d9cdbf3 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 07:47:52 +0100 Subject: [PATCH 01/15] Bump project-beta-automations to v2.0.0 (#4) --- .github/workflows/add-content-to-project.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/add-content-to-project.yml b/.github/workflows/add-content-to-project.yml index 02bca6e..6c1af2d 100644 --- a/.github/workflows/add-content-to-project.yml +++ b/.github/workflows/add-content-to-project.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Set Issue to 'Priority = Triage Next'" - uses: leonsteinhaeuser/project-beta-automations@v1.2.1 + uses: leonsteinhaeuser/:@v2.0.0 if: github.event_name == 'issues' with: gh_token: ${{ secrets.TF_DEVEX_PROJECT_GITHUB_TOKEN }} @@ -29,7 +29,7 @@ jobs: operation_mode: custom_field custom_field_values: '[{\"name\":\"Priority\",\"type\":\"single_select\",\"value\":\"Triage Next\"}]' - name: "Set Pull Request to 'Priority = Triage Next'" - uses: leonsteinhaeuser/project-beta-automations@v1.2.1 + uses: leonsteinhaeuser/project-beta-automations@v2.0.0 if: github.event_name == 'pull_request_target' with: gh_token: ${{ secrets.TF_DEVEX_PROJECT_GITHUB_TOKEN }} From bd8d6f63a3bfb614594a3ecace921ea56b954a44 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 09:23:15 +0100 Subject: [PATCH 02/15] Adding schema mutation functions (#4) --- Makefile | 15 +++ go.mod | 22 +++++ go.sum | 73 +++++++++++++++ timeouts/schema.go | 110 ++++++++++++++++++++++ timeouts/schema_test.go | 200 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 420 insertions(+) create mode 100644 Makefile create mode 100644 go.sum create mode 100644 timeouts/schema.go create mode 100644 timeouts/schema_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cf2a4c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +default: build + +build: + go build -v ./... + +lint: + golangci-lint run + +fmt: + gofmt -s -w -e . + +test: + go test -v -cover -timeout=120s -parallel=4 ./... + +.PHONY: build lint fmt test diff --git a/go.mod b/go.mod index fa19ddd..d0d34ca 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,25 @@ module github.com/hashicorp/terraform-plugin-framework-timeouts go 1.18 + +require ( + github.com/google/go-cmp v0.5.9 + github.com/hashicorp/terraform-plugin-framework v0.12.1-0.20220913180647-04ff77076480 +) + +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.0 // 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 + google.golang.org/appengine v1.6.5 // indirect + google.golang.org/protobuf v1.28.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c86d7c --- /dev/null +++ b/go.sum @@ -0,0 +1,73 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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.12.0 h1:Bk3l5MQUaZoo5eplr+u1FomYqGS564e8Tp3rutnCfYg= +github.com/hashicorp/terraform-plugin-framework v0.12.0/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= +github.com/hashicorp/terraform-plugin-framework v0.12.1-0.20220913180647-04ff77076480 h1:0DK3VsMnVD0lDuUCq16bAkwfpiyC3/z9/Lp1iUcBogI= +github.com/hashicorp/terraform-plugin-framework v0.12.1-0.20220913180647-04ff77076480/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= +github.com/hashicorp/terraform-plugin-go v0.14.0 h1:ttnSlS8bz3ZPYbMb84DpcPhY4F5DsQtcAS7cHo8uvP4= +github.com/hashicorp/terraform-plugin-go v0.14.0/go.mod h1:2nNCBeRLaenyQEi78xrGrs9hMbulveqG/zDMQSvVJTE= +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= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +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= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +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/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/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= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/timeouts/schema.go b/timeouts/schema.go new file mode 100644 index 0000000..3a6b356 --- /dev/null +++ b/timeouts/schema.go @@ -0,0 +1,110 @@ +package timeouts + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +const ( + create = "create" + read = "read" + update = "update" + del = "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{} + + if opts.Create { + attributes[create] = tfsdk.Attribute{ + Type: types.StringType, + Optional: true, + // TODO: Add Validators: for checking that string parses as time.Duration + } + } + + if opts.Read { + attributes[read] = tfsdk.Attribute{ + Type: types.StringType, + Optional: true, + // TODO: Add Validators: for checking that string parses as time.Duration + } + } + + if opts.Update { + attributes[update] = tfsdk.Attribute{ + Type: types.StringType, + Optional: true, + // TODO: Add Validators: for checking that string parses as time.Duration + } + } + + if opts.Delete { + attributes[del] = tfsdk.Attribute{ + Type: types.StringType, + Optional: true, + // TODO: Add Validators: for checking that string parses as time.Duration + } + } + + return attributes +} diff --git a/timeouts/schema_test.go b/timeouts/schema_test.go new file mode 100644 index 0000000..df06357 --- /dev/null +++ b/timeouts/schema_test.go @@ -0,0 +1,200 @@ +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/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, + }, + }, + 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, + }, + "update": { + Type: types.StringType, + Optional: true, + }, + }, + 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, + }, + "read": { + Type: types.StringType, + Optional: true, + }, + "update": { + Type: types.StringType, + Optional: true, + }, + "delete": { + Type: types.StringType, + Optional: true, + }, + }, + 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, + }, + }), + }, + }, + "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, + }, + "update": { + Type: 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 := tfsdk.Attribute{ + Optional: true, + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "create": { + Type: types.StringType, + Optional: true, + }, + "read": { + Type: types.StringType, + Optional: true, + }, + "update": { + Type: types.StringType, + Optional: true, + }, + "delete": { + Type: types.StringType, + Optional: true, + }, + }), + } + + if diff := cmp.Diff(actual, expected); diff != "" { + t.Errorf("unexpected block difference: %s", diff) + } +} From b5b60f8288f9fcf4cfba8f6bfd2537d233a6f594 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 10:05:34 +0100 Subject: [PATCH 03/15] Adding validator to verify timeout attributes are parseable as time.Duration (#4) --- internal/validators/timeduration.go | 66 ++++++++++++++++++++++++ internal/validators/timeduration_test.go | 60 +++++++++++++++++++++ timeouts/schema.go | 21 +++++--- timeouts/schema_test.go | 43 +++++++++++++++ 4 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 internal/validators/timeduration.go create mode 100644 internal/validators/timeduration_test.go diff --git a/internal/validators/timeduration.go b/internal/validators/timeduration.go new file mode 100644 index 0000000..232d197 --- /dev/null +++ b/internal/validators/timeduration.go @@ -0,0 +1,66 @@ +package validators + +import ( + "context" + "time" + "unicode" + "unicode/utf8" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ tfsdk.AttributeValidator = timeDurationValidator{} + +// timeDurationValidator validates that a string Attribute's value is parseable as time.Duration. +type timeDurationValidator struct { +} + +// Description describes the validation in plain text formatting. +func (validator timeDurationValidator) Description(_ context.Context) string { + return "string must be parseable as time.Duration" +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator timeDurationValidator) MarkdownDescription(ctx context.Context) string { + 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) + + if s.Unknown || s.Null { + return + } + + if _, err := time.ParseDuration(s.Value); err != nil { + response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + request.AttributePath, + "Invalid Attribute Value Time Duration", + capitalize(validator.Description(ctx))+", got: "+s.Value)) + return + } +} + +// TimeDuration 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 TimeDuration() tfsdk.AttributeValidator { + return timeDurationValidator{} +} + +// capitalize will uppercase the first letter in a UTF-8 string. +func capitalize(str string) string { + if str == "" { + return "" + } + + firstRune, size := utf8.DecodeRuneInString(str) + + return string(unicode.ToUpper(firstRune)) + str[size:] +} diff --git a/internal/validators/timeduration_test.go b/internal/validators/timeduration_test.go new file mode 100644 index 0000000..27fc88a --- /dev/null +++ b/internal/validators/timeduration_test.go @@ -0,0 +1,60 @@ +package validators_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" +) + +func TestTimeDuration(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectError bool + } + + tests := map[string]testCase{ + "unknown": { + val: types.String{Unknown: true}, + }, + "null": { + val: types.String{Null: true}, + }, + "valid": { + val: types.String{Value: "20m"}, + }, + "invalid": { + val: types.String{Value: "20x"}, + expectError: true, + }, + } + + 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 !response.Diagnostics.HasError() && test.expectError { + t.Fatal("expected error, got no error") + } + + if response.Diagnostics.HasError() && !test.expectError { + t.Fatalf("got unexpected error: %s", response.Diagnostics) + } + }) + } +} diff --git a/timeouts/schema.go b/timeouts/schema.go index 3a6b356..975c524 100644 --- a/timeouts/schema.go +++ b/timeouts/schema.go @@ -5,6 +5,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/internal/validators" ) const ( @@ -78,7 +80,9 @@ func attributesMap(opts Opts) map[string]tfsdk.Attribute { attributes[create] = tfsdk.Attribute{ Type: types.StringType, Optional: true, - // TODO: Add Validators: for checking that string parses as time.Duration + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }, } } @@ -86,24 +90,27 @@ func attributesMap(opts Opts) map[string]tfsdk.Attribute { attributes[read] = tfsdk.Attribute{ Type: types.StringType, Optional: true, - // TODO: Add Validators: for checking that string parses as time.Duration - } + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }} } if opts.Update { attributes[update] = tfsdk.Attribute{ Type: types.StringType, Optional: true, - // TODO: Add Validators: for checking that string parses as time.Duration - } + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }} } if opts.Delete { attributes[del] = tfsdk.Attribute{ Type: types.StringType, Optional: true, - // TODO: Add Validators: for checking that string parses as time.Duration - } + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }} } return attributes diff --git a/timeouts/schema_test.go b/timeouts/schema_test.go index df06357..af10dc5 100644 --- a/timeouts/schema_test.go +++ b/timeouts/schema_test.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -35,6 +36,9 @@ func TestBlock(t *testing.T) { "create": { Type: types.StringType, Optional: true, + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }, }, }, NestingMode: tfsdk.BlockNestingModeSingle, @@ -50,10 +54,16 @@ func TestBlock(t *testing.T) { "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, @@ -83,18 +93,30 @@ func TestBlockAll(t *testing.T) { "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, @@ -130,6 +152,9 @@ func TestAttributes(t *testing.T) { "create": { Type: types.StringType, Optional: true, + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }, }, }), }, @@ -145,10 +170,16 @@ func TestAttributes(t *testing.T) { "create": { Type: types.StringType, Optional: true, + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }, }, "update": { Type: types.StringType, Optional: true, + Validators: []tfsdk.AttributeValidator{ + validators.TimeDuration(), + }, }, }), }, @@ -178,18 +209,30 @@ func TestAttributesAll(t *testing.T) { "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(), + }, }, }), } From 34d17857d6fbf03bc11240331dd85dfa1a24a8f9 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 10:15:42 +0100 Subject: [PATCH 04/15] Updating go-version and goreleaser-action (#4) --- .github/workflows/ci-github-actions.yml | 2 +- .github/workflows/ci-go.yml | 2 +- .github/workflows/ci-goreleaser.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index ba79046..092b706 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -19,6 +19,6 @@ jobs: run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} + go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest - run: actionlint diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index ddc03c4..199f840 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.18', '1.17' ] + go-version: [ '1.19', '1.18' ] steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index ecf7740..b2a22bc 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -19,7 +19,7 @@ jobs: run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} - - uses: goreleaser/goreleaser-action@v2 + go-version-file: 'go.mod' + - uses: goreleaser/goreleaser-action@v3 with: args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f37e2a..0bd1e16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,11 +24,11 @@ jobs: run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} + go-version-file: 'go.mod' - name: Generate Release Notes # Fetch CHANGELOG.md contents up to Git tag prior to this release, skipping top two lines run: sed -n -e "1{/# /d;}" -e "2{/^$/d;}" -e "/# $(git describe --abbrev=0 --exclude="$(git describe --abbrev=0 --match='v*.*.*' --tags)" --match='v*.*.*' --tags | tr -d v)/q;p" CHANGELOG.md > /tmp/release-notes.txt - - uses: goreleaser/goreleaser-action@v2 + - uses: goreleaser/goreleaser-action@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 4c76f05e84086de2010cfcc07711231d91cdd83c Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 10:17:57 +0100 Subject: [PATCH 05/15] Adding CHANGELOG entry (#4) --- .changelog/5.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/5.txt diff --git a/.changelog/5.txt b/.changelog/5.txt new file mode 100644 index 0000000..232d3a8 --- /dev/null +++ b/.changelog/5.txt @@ -0,0 +1,3 @@ +```release-note:feature +Introduced `timeouts` package with `Block()`, `BlockAll()`, `Attributes()` and `AttributesAll()` schema mutation functions +``` \ No newline at end of file From d8342ff7276fa560f1c64b8279096140a69ad20b Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 10:24:37 +0100 Subject: [PATCH 06/15] Updating ci-go.yml (#4) --- .github/workflows/ci-go.yml | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 199f840..2ca040a 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -21,26 +21,8 @@ jobs: with: go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@v3.2.0 - with: - skip-go-installation: true - terraform-provider-corner: - defaults: - run: - working-directory: terraform-provider-corner - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/checkout@v3 - with: - path: terraform-provider-corner - repository: hashicorp/terraform-provider-corner - - uses: actions/setup-go@v3 - with: - go-version-file: 'go.mod' - - run: go mod edit -replace=github.com/hashicorp/terraform-plugin-framework-timeouts=../ - - run: go mod tidy - - run: go test -v ./internal/frameworkprovider + - uses: golangci/golangci-lint-action@v3 + test: name: test (Go v${{ matrix.go-version }}) runs-on: ubuntu-latest From a156126a78e3afefc50f1592d3e5b8c486675bc6 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 10:27:41 +0100 Subject: [PATCH 07/15] Fix add-content-to-project.yml (#4) --- .github/workflows/add-content-to-project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/add-content-to-project.yml b/.github/workflows/add-content-to-project.yml index 6c1af2d..30e89c5 100644 --- a/.github/workflows/add-content-to-project.yml +++ b/.github/workflows/add-content-to-project.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Set Issue to 'Priority = Triage Next'" - uses: leonsteinhaeuser/:@v2.0.0 + uses: leonsteinhaeuser/project-beta-automations@v2.0.0 if: github.event_name == 'issues' with: gh_token: ${{ secrets.TF_DEVEX_PROJECT_GITHUB_TOKEN }} From 5491df75b175542bf20c6d28df3e287d815060e6 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 14:23:09 +0100 Subject: [PATCH 08/15] DRY-up attributes (#4) --- timeouts/schema.go | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/timeouts/schema.go b/timeouts/schema.go index 975c524..69b6c20 100644 --- a/timeouts/schema.go +++ b/timeouts/schema.go @@ -75,42 +75,28 @@ func AttributesAll(ctx context.Context) tfsdk.Attribute { 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[create] = tfsdk.Attribute{ - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }, - } + attributes[create] = attribute } if opts.Read { - attributes[read] = tfsdk.Attribute{ - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }} + attributes[read] = attribute } if opts.Update { - attributes[update] = tfsdk.Attribute{ - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }} + attributes[update] = attribute } if opts.Delete { - attributes[del] = tfsdk.Attribute{ - Type: types.StringType, - Optional: true, - Validators: []tfsdk.AttributeValidator{ - validators.TimeDuration(), - }} + attributes[del] = attribute } return attributes From 86b2bec35b8d5d5d914a3f92c59228f397e21969 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 16:11:26 +0100 Subject: [PATCH 09/15] Adding helper functions to obtain time.Duration for create, read, update and delete timeouts (#4) --- timeouts/type.go | 185 ++++++++++++ timeouts/type_test.go | 674 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 859 insertions(+) create mode 100644 timeouts/type.go create mode 100644 timeouts/type_test.go diff --git a/timeouts/type.go b/timeouts/type.go new file mode 100644 index 0000000..09c242a --- /dev/null +++ b/timeouts/type.go @@ -0,0 +1,185 @@ +package timeouts + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func Create(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + + if _, ok := obj.Attrs[create]; !ok { + diags.AddError( + "Create Timeout Not Found", + "Create timeout is not present within the timeouts") + return nil, diags + } + + createTimeout := obj.Attrs[create] + + // Although the schema mutation functions guarantee that the type for create timeout + // is a string, this function accepts any types.Object. + if _, ok := createTimeout.(types.String); !ok { + diags.AddError( + "Create Timeout Not String", + "Create timeout must be a string") + return nil, diags + } + + // 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(createTimeout.(types.String).Value) + if err != nil { + diags.AddError( + "Create Timeout Not Parseable", + "Create timeout cannot be parsed as time.Duration") + return nil, diags + } + + return &duration, nil +} + +func CreateDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { + duration, diags := Create(ctx, obj) + + if diags.HasError() { + return def + } + + return *duration +} + +func Read(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + + if _, ok := obj.Attrs[read]; !ok { + diags.AddError( + "Read Timeout Not Found", + "Read timeout is not present within the timeouts") + return nil, diags + } + + readTimeout := obj.Attrs[read] + + // Although the schema mutation functions guarantee that the type for read timeout + // is a string, this function accepts any types.Object. + if _, ok := readTimeout.(types.String); !ok { + diags.AddError( + "Read Timeout Not String", + "Read timeout must be a string") + return nil, diags + } + + // 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(readTimeout.(types.String).Value) + if err != nil { + diags.AddError( + "Read Timeout Not Parseable", + "Read timeout cannot be parsed as time.Duration") + return nil, diags + } + + return &duration, nil +} + +func ReadDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { + duration, diags := Read(ctx, obj) + + if diags.HasError() { + return def + } + + return *duration +} + +func Update(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + + if _, ok := obj.Attrs[update]; !ok { + diags.AddError( + "Update Timeout Not Found", + "Update timeout is not present within the timeouts") + return nil, diags + } + + updateTimeout := obj.Attrs[update] + + // Although the schema mutation functions guarantee that the type for update timeout + // is a string, this function accepts any types.Object. + if _, ok := updateTimeout.(types.String); !ok { + diags.AddError( + "Update Timeout Not String", + "Update timeout must be a string") + return nil, diags + } + + // 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(updateTimeout.(types.String).Value) + if err != nil { + diags.AddError( + "Update Timeout Not Parseable", + "Update timeout cannot be parsed as time.Duration") + return nil, diags + } + + return &duration, nil +} + +func UpdateDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { + duration, diags := Update(ctx, obj) + + if diags.HasError() { + return def + } + + return *duration +} + +func Delete(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { + var diags diag.Diagnostics + + if _, ok := obj.Attrs[del]; !ok { + diags.AddError( + "Delete Timeout Not Found", + "Delete timeout is not present within the timeouts") + return nil, diags + } + + deleteTimeout := obj.Attrs[del] + + // Although the schema mutation functions guarantee that the type for delete timeout + // is a string, this function accepts any types.Object. + if _, ok := deleteTimeout.(types.String); !ok { + diags.AddError( + "Delete Timeout Not String", + "Delete timeout must be a string") + return nil, diags + } + + // 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(deleteTimeout.(types.String).Value) + if err != nil { + diags.AddError( + "Delete Timeout Not Parseable", + "Delete timeout cannot be parsed as time.Duration") + return nil, diags + } + + return &duration, nil +} + +func DeleteDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { + duration, diags := Delete(ctx, obj) + + if diags.HasError() { + return def + } + + return *duration +} diff --git a/timeouts/type_test.go b/timeouts/type_test.go new file mode 100644 index 0000000..257843c --- /dev/null +++ b/timeouts/type_test.go @@ -0,0 +1,674 @@ +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-framework-timeouts/timeouts" +) + +func TestType_Create(t *testing.T) { + t.Parallel() + + type testCase struct { + obj types.Object + expected *time.Duration + expectedDiag diag.Diagnostic + } + + tests := map[string]testCase{ + "create-not-present": { + obj: types.Object{}, + expectedDiag: diag.NewErrorDiagnostic( + "Create Timeout Not Found", + "Create timeout is not present within the timeouts", + ), + }, + "create-not-string": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.Bool{}, + }, + AttrTypes: map[string]attr.Type{ + "create": types.BoolType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Create Timeout Not String", + "Create timeout must be a string", + ), + }, + "create-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Create Timeout Not Parseable", + "Create timeout cannot be parsed as time.Duration", + ), + }, + "create-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Create Timeout Not Parseable", + "Create timeout cannot be parsed as time.Duration", + ), + }, + "create-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + expected: ptr(60 * time.Minute), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + expectedDiags := diag.Diagnostics{} + expectedDiags.Append(test.expectedDiag) + + actual, diags := timeouts.Create(context.Background(), test.obj) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + + if diff := cmp.Diff(diags, expectedDiags); diff != "" { + t.Errorf("unexpected diags difference: %s", diff) + } + }) + } +} + +func TestType_CreateDefault(t *testing.T) { + t.Parallel() + + defaultTimeout := 20 * time.Minute + + type testCase struct { + obj types.Object + expected time.Duration + } + + tests := map[string]testCase{ + "create-not-present": { + expected: defaultTimeout, + }, + "create-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "create-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "create-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + expected: 60 * time.Minute, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := timeouts.CreateDefault(context.Background(), test.obj, defaultTimeout) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + }) + } +} + +func TestType_Read(t *testing.T) { + t.Parallel() + + type testCase struct { + obj types.Object + expected *time.Duration + expectedDiag diag.Diagnostic + } + + tests := map[string]testCase{ + "read-not-present": { + obj: types.Object{}, + expectedDiag: diag.NewErrorDiagnostic( + "Read Timeout Not Found", + "Read timeout is not present within the timeouts", + ), + }, + "read-not-string": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "read": types.Bool{}, + }, + AttrTypes: map[string]attr.Type{ + "read": types.BoolType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Read Timeout Not String", + "Read timeout must be a string", + ), + }, + "read-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "read": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Read Timeout Not Parseable", + "Read timeout cannot be parsed as time.Duration", + ), + }, + "read-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "read": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Read Timeout Not Parseable", + "Read timeout cannot be parsed as time.Duration", + ), + }, + "read-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "read": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + expected: ptr(60 * time.Minute), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + expectedDiags := diag.Diagnostics{} + expectedDiags.Append(test.expectedDiag) + + actual, diags := timeouts.Read(context.Background(), test.obj) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + + if diff := cmp.Diff(diags, expectedDiags); diff != "" { + t.Errorf("unexpected diags difference: %s", diff) + } + }) + } +} + +func TestType_ReadDefault(t *testing.T) { + t.Parallel() + + defaultTimeout := 20 * time.Minute + + type testCase struct { + obj types.Object + expected time.Duration + } + + tests := map[string]testCase{ + "read-not-present": { + expected: defaultTimeout, + }, + "read-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "read": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "read-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "read": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "read-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "read": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "read": types.StringType, + }, + }, + expected: 60 * time.Minute, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := timeouts.ReadDefault(context.Background(), test.obj, defaultTimeout) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + }) + } +} + +func TestType_Update(t *testing.T) { + t.Parallel() + + type testCase struct { + obj types.Object + expected *time.Duration + expectedDiag diag.Diagnostic + } + + tests := map[string]testCase{ + "update-not-present": { + obj: types.Object{}, + expectedDiag: diag.NewErrorDiagnostic( + "Update Timeout Not Found", + "Update timeout is not present within the timeouts", + ), + }, + "update-not-string": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "update": types.Bool{}, + }, + AttrTypes: map[string]attr.Type{ + "update": types.BoolType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Update Timeout Not String", + "Update timeout must be a string", + ), + }, + "update-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "update": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "update": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Update Timeout Not Parseable", + "Update timeout cannot be parsed as time.Duration", + ), + }, + "update-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "update": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "update": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Update Timeout Not Parseable", + "Update timeout cannot be parsed as time.Duration", + ), + }, + "update-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "update": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "update": types.StringType, + }, + }, + expected: ptr(60 * time.Minute), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + expectedDiags := diag.Diagnostics{} + expectedDiags.Append(test.expectedDiag) + + actual, diags := timeouts.Update(context.Background(), test.obj) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + + if diff := cmp.Diff(diags, expectedDiags); diff != "" { + t.Errorf("unexpected diags difference: %s", diff) + } + }) + } +} + +func TestType_UpdateDefault(t *testing.T) { + t.Parallel() + + defaultTimeout := 20 * time.Minute + + type testCase struct { + obj types.Object + expected time.Duration + } + + tests := map[string]testCase{ + "update-not-present": { + expected: defaultTimeout, + }, + "update-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "update": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "update": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "update-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "update": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "update": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "update-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "update": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "update": types.StringType, + }, + }, + expected: 60 * time.Minute, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := timeouts.UpdateDefault(context.Background(), test.obj, defaultTimeout) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + }) + } +} + +func TestType_Delete(t *testing.T) { + t.Parallel() + + type testCase struct { + obj types.Object + expected *time.Duration + expectedDiag diag.Diagnostic + } + + tests := map[string]testCase{ + "delete-not-present": { + obj: types.Object{}, + expectedDiag: diag.NewErrorDiagnostic( + "Delete Timeout Not Found", + "Delete timeout is not present within the timeouts", + ), + }, + "delete-not-string": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "delete": types.Bool{}, + }, + AttrTypes: map[string]attr.Type{ + "delete": types.BoolType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Delete Timeout Not String", + "Delete timeout must be a string", + ), + }, + "delete-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "delete": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "delete": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Delete Timeout Not Parseable", + "Delete timeout cannot be parsed as time.Duration", + ), + }, + "delete-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "delete": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "delete": types.StringType, + }, + }, + expectedDiag: diag.NewErrorDiagnostic( + "Delete Timeout Not Parseable", + "Delete timeout cannot be parsed as time.Duration", + ), + }, + "delete-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "delete": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "delete": types.StringType, + }, + }, + expected: ptr(60 * time.Minute), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + expectedDiags := diag.Diagnostics{} + expectedDiags.Append(test.expectedDiag) + + actual, diags := timeouts.Delete(context.Background(), test.obj) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + + if diff := cmp.Diff(diags, expectedDiags); diff != "" { + t.Errorf("unexpected diags difference: %s", diff) + } + }) + } +} + +func TestType_DeleteDefault(t *testing.T) { + t.Parallel() + + defaultTimeout := 20 * time.Minute + + type testCase struct { + obj types.Object + expected time.Duration + } + + tests := map[string]testCase{ + "delete-not-present": { + expected: defaultTimeout, + }, + "delete-not-parseable-empty": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "delete": types.String{ + Value: "", + }, + }, + AttrTypes: map[string]attr.Type{ + "delete": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "delete-not-parseable": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "delete": types.String{ + Value: "60x", + }, + }, + AttrTypes: map[string]attr.Type{ + "delete": types.StringType, + }, + }, + expected: defaultTimeout, + }, + "delete-valid": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "delete": types.String{ + Value: "60m", + }, + }, + AttrTypes: map[string]attr.Type{ + "delete": types.StringType, + }, + }, + expected: 60 * time.Minute, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + actual := timeouts.DeleteDefault(context.Background(), test.obj, defaultTimeout) + + if diff := cmp.Diff(actual, test.expected); diff != "" { + t.Errorf("unexpected duration difference: %s", diff) + } + }) + } +} + +func ptr[T any](v T) *T { + return &v +} From b5169b1eb90cb7900a6b68e8063fc7ad119ee6a6 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 14 Sep 2022 16:33:44 +0100 Subject: [PATCH 10/15] Adding comments (#4) --- timeouts/{type.go => parser.go} | 16 ++++++++++++++++ timeouts/{type_test.go => parser_test.go} | 0 2 files changed, 16 insertions(+) rename timeouts/{type.go => parser.go} (79%) rename timeouts/{type_test.go => parser_test.go} (100%) diff --git a/timeouts/type.go b/timeouts/parser.go similarity index 79% rename from timeouts/type.go rename to timeouts/parser.go index 09c242a..e584ce9 100644 --- a/timeouts/type.go +++ b/timeouts/parser.go @@ -8,6 +8,8 @@ import ( "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. func Create(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { var diags diag.Diagnostics @@ -42,6 +44,8 @@ func Create(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnos return &duration, nil } +// CreateDefault returns time.Duration generated from parsing the "create" value in obj.Attrs +// or the supplied default if "create" cannot be found or parsed. func CreateDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { duration, diags := Create(ctx, obj) @@ -52,6 +56,8 @@ func CreateDefault(ctx context.Context, obj types.Object, def time.Duration) tim 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. func Read(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { var diags diag.Diagnostics @@ -86,6 +92,8 @@ func Read(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnosti return &duration, nil } +// ReadDefault returns time.Duration generated from parsing the "read" value in obj.Attrs +// or the supplied default if "read" cannot be found or parsed. func ReadDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { duration, diags := Read(ctx, obj) @@ -96,6 +104,8 @@ func ReadDefault(ctx context.Context, obj types.Object, def time.Duration) time. 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. func Update(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { var diags diag.Diagnostics @@ -130,6 +140,8 @@ func Update(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnos return &duration, nil } +// UpdateDefault returns time.Duration generated from parsing the "update" value in obj.Attrs +// or the supplied default if "update" cannot be found or parsed. func UpdateDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { duration, diags := Update(ctx, obj) @@ -140,6 +152,8 @@ func UpdateDefault(ctx context.Context, obj types.Object, def time.Duration) tim 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. func Delete(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { var diags diag.Diagnostics @@ -174,6 +188,8 @@ func Delete(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnos return &duration, nil } +// DeleteDefault returns time.Duration generated from parsing the "delete" value in obj.Attrs +// or the supplied default if "delete" cannot be found or parsed. func DeleteDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { duration, diags := Delete(ctx, obj) diff --git a/timeouts/type_test.go b/timeouts/parser_test.go similarity index 100% rename from timeouts/type_test.go rename to timeouts/parser_test.go From 955756ad1a118feca832dbf509884927e7764728 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 15 Sep 2022 10:11:41 +0100 Subject: [PATCH 11/15] Adding example usage to README (#4) --- README.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/README.md b/README.md index c571e6e..25b5e6e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,122 @@ This Go module follows `terraform-plugin-framework` Go compatibility. Currently that means Go **1.18** must be used when developing and testing code. +## Usage + +Usage of this module requires the following changes in the provider code: + +- [Schema Mutation](#schema-mutation) +- [Updating Models](#updating-models) +- Accessing Timeouts in CRUD Functions + +### Schema Mutation + +#### Block + +If your configuration is using a nested block to define timeouts, such as the following: + +```terraform +resource "timeouts_example" "example" { + /* ... */ + + timeouts { + create = "60m" + } +} +``` + +You can use this module to mutate the `tfsdk.Schema` as follows: + +```go +func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + /* ... */ + + Blocks: map[string]tfsdk.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + }), + }, +``` + +#### Attribute + +```terraform +resource "timeouts_example" "example" { + /* ... */ + + timeouts = { + create = "60m" + } +} +``` + +You can use this module to mutate the `tfsdk.Schema` as follows: + +```go +func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + /* ... */ + "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ + Create: true, + }), + }, +``` + +### 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 + + diags := req.Config.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: + +```go +type exampleResourceData struct { + /* ... */ + Timeouts types.Object `tfsdk:"timeouts"` +``` + +### Accessing Timeouts in CRUD Functions + +Once the model has been populated with the config, state or plan the duration of the timeout can be accessed by calling +the appropriate helper function and then used to configure timeout behaviour, for instance: + +```go +func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data exampleResourceData + + diags := req.Config.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + createTimeout, diags := timeouts.Create(ctx, data.Timeouts) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if createTimeout == nil { + /* ... */ + } + + ctx, cancel := context.WithTimeout(ctx, *createTimeout) + defer cancel() + + /* ... */ +} +``` + ## Contributing See [`.github/CONTRIBUTING.md`](https://github.com/hashicorp/terraform-plugin-framework-timeouts/blob/main/.github/CONTRIBUTING.md) From 04275ae5ad722c62c892307797c3423956d4516b Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Thu, 15 Sep 2022 14:12:52 +0100 Subject: [PATCH 12/15] Formatting README (#4) --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 25b5e6e..85fa3b5 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Usage of this module requires the following changes in the provider code: - [Schema Mutation](#schema-mutation) - [Updating Models](#updating-models) -- Accessing Timeouts in CRUD Functions +- [Accessing Timeouts in CRUD Functions](#accessing-timeouts-in-crud-functions) ### Schema Mutation @@ -54,6 +54,8 @@ func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Dia #### Attribute +If your configuration is using nested attributes to define timeouts, such as the following: + ```terraform resource "timeouts_example" "example" { /* ... */ @@ -71,9 +73,9 @@ func (t *exampleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Dia return tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ /* ... */ - "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ + "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ Create: true, - }), + }), }, ``` @@ -95,7 +97,7 @@ timeouts which is of `types.Object`. For example: ```go type exampleResourceData struct { /* ... */ - Timeouts types.Object `tfsdk:"timeouts"` + Timeouts types.Object `tfsdk:"timeouts"` ``` ### Accessing Timeouts in CRUD Functions @@ -119,7 +121,7 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, return } - if createTimeout == nil { + if createTimeout == nil { /* ... */ } From 9c22cea12490d6dccca9adcb72de2637d89d34cd Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 21 Sep 2022 09:48:08 +0100 Subject: [PATCH 13/15] Updates following code review (#4) --- .github/workflows/ci-github-actions.yml | 3 - .github/workflows/ci-goreleaser.yml | 3 - .github/workflows/release.yml | 3 - README.md | 19 +- go.mod | 2 +- go.sum | 6 +- internal/validators/timeduration.go | 19 +- internal/validators/timeduration_test.go | 24 +- timeouts/parser.go | 163 +++------ timeouts/parser_test.go | 429 ++++------------------- timeouts/schema.go | 16 +- 11 files changed, 153 insertions(+), 534 deletions(-) diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index 092b706..0b6e2d3 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,9 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: go-version-file: 'go.mod' diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index b2a22bc..c47009a 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,9 +14,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: go-version-file: 'go.mod' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0bd1e16..b245304 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,9 +19,6 @@ jobs: with: # Required for release notes fetch-depth: 0 - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: go-version-file: 'go.mod' diff --git a/README.md b/README.md index 85fa3b5..89dd492 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This Go module is typically kept up to date with the latest `terraform-plugin-fr This Go module follows `terraform-plugin-framework` Go compatibility. -Currently that means Go **1.18** must be used when developing and testing code. +Currently, that means Go **1.18** must be used when developing and testing code. ## Usage @@ -24,6 +24,16 @@ Usage of this module requires the following changes in the provider code: ### Schema Mutation +Timeouts can be defined using either nested blocks or nested attributes. + +If you are writing a new provider using [terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) +then we recommend using nested attributes. + +If you are [migrating a provider from SDKv2 to the Framework](https://www.terraform.io/plugin/framework/migrating) and +you are already using timeouts you can either continue to use block syntax, or switch to using nested attributes. +However, switching to using nested attributes will require that practitioners that are using your provider update their +Terraform configuration. + #### Block If your configuration is using a nested block to define timeouts, such as the following: @@ -87,7 +97,7 @@ In functions in which the config, state or plan is being unmarshalled, for insta func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data exampleResourceData - diags := req.Config.Get(ctx, &data) + diags := req.Plan.Get(ctx, &data) resp.Diagnostics.Append(diags...) ``` @@ -109,13 +119,14 @@ the appropriate helper function and then used to configure timeout behaviour, fo func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data exampleResourceData - diags := req.Config.Get(ctx, &data) + diags := req.Plan.Get(ctx, &data) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - createTimeout, diags := timeouts.Create(ctx, data.Timeouts) + defaultCreateTimeout := 20 * time.Minutes + createTimeout, diags := timeouts.Create(ctx, data.Timeouts, defaultCreateTimeout) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/go.mod b/go.mod index d0d34ca..0014830 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/terraform-plugin-framework v0.12.1-0.20220913180647-04ff77076480 + github.com/hashicorp/terraform-plugin-framework v0.13.0 ) require ( diff --git a/go.sum b/go.sum index 6c86d7c..63e2aee 100644 --- a/go.sum +++ b/go.sum @@ -13,10 +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.12.0 h1:Bk3l5MQUaZoo5eplr+u1FomYqGS564e8Tp3rutnCfYg= -github.com/hashicorp/terraform-plugin-framework v0.12.0/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= -github.com/hashicorp/terraform-plugin-framework v0.12.1-0.20220913180647-04ff77076480 h1:0DK3VsMnVD0lDuUCq16bAkwfpiyC3/z9/Lp1iUcBogI= -github.com/hashicorp/terraform-plugin-framework v0.12.1-0.20220913180647-04ff77076480/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= +github.com/hashicorp/terraform-plugin-framework v0.13.0 h1:tGnqttzZwU3FKc+HasHr2Yi5L81FcQbdc8zQhbBD9jQ= +github.com/hashicorp/terraform-plugin-framework v0.13.0/go.mod h1:wcZdk4+Uef6Ng+BiBJjGAcIPlIs5bhlEV/TA1k6Xkq8= github.com/hashicorp/terraform-plugin-go v0.14.0 h1:ttnSlS8bz3ZPYbMb84DpcPhY4F5DsQtcAS7cHo8uvP4= github.com/hashicorp/terraform-plugin-go v0.14.0/go.mod h1:2nNCBeRLaenyQEi78xrGrs9hMbulveqG/zDMQSvVJTE= github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs= diff --git a/internal/validators/timeduration.go b/internal/validators/timeduration.go index 232d197..7847638 100644 --- a/internal/validators/timeduration.go +++ b/internal/validators/timeduration.go @@ -2,9 +2,8 @@ package validators import ( "context" + "fmt" "time" - "unicode" - "unicode/utf8" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -19,7 +18,7 @@ type timeDurationValidator struct { // Description describes the validation in plain text formatting. func (validator timeDurationValidator) Description(_ context.Context) string { - return "string must be parseable as time.Duration" + return `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".` } // MarkdownDescription describes the validation in Markdown formatting. @@ -39,7 +38,8 @@ func (validator timeDurationValidator) Validate(ctx context.Context, request tfs response.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( request.AttributePath, "Invalid Attribute Value Time Duration", - capitalize(validator.Description(ctx))+", got: "+s.Value)) + fmt.Sprintf("%q %s", s.Value, validator.Description(ctx))), + ) return } } @@ -53,14 +53,3 @@ func (validator timeDurationValidator) Validate(ctx context.Context, request tfs func TimeDuration() tfsdk.AttributeValidator { return timeDurationValidator{} } - -// capitalize will uppercase the first letter in a UTF-8 string. -func capitalize(str string) string { - if str == "" { - return "" - } - - firstRune, size := utf8.DecodeRuneInString(str) - - return string(unicode.ToUpper(firstRune)) + str[size:] -} diff --git a/internal/validators/timeduration_test.go b/internal/validators/timeduration_test.go index 27fc88a..b051609 100644 --- a/internal/validators/timeduration_test.go +++ b/internal/validators/timeduration_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "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/types" @@ -15,8 +17,8 @@ func TestTimeDuration(t *testing.T) { t.Parallel() type testCase struct { - val types.String - expectError bool + val types.String + expectedDiagnostics diag.Diagnostics } tests := map[string]testCase{ @@ -30,8 +32,14 @@ func TestTimeDuration(t *testing.T) { val: types.String{Value: "20m"}, }, "invalid": { - val: types.String{Value: "20x"}, - expectError: true, + val: types.String{Value: "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".`, + ), + }, }, } @@ -48,12 +56,8 @@ func TestTimeDuration(t *testing.T) { validators.TimeDuration().Validate(context.Background(), request, &response) - if !response.Diagnostics.HasError() && test.expectError { - t.Fatal("expected error, got no error") - } - - if response.Diagnostics.HasError() && !test.expectError { - t.Fatalf("got unexpected error: %s", response.Diagnostics) + if diff := cmp.Diff(response.Diagnostics, test.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) } }) } diff --git a/timeouts/parser.go b/timeouts/parser.go index e584ce9..7eeab79 100644 --- a/timeouts/parser.go +++ b/timeouts/parser.go @@ -4,198 +4,125 @@ import ( "context" "time" - "github.com/hashicorp/terraform-plugin-framework/diag" "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. -func Create(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { - var diags diag.Diagnostics - - if _, ok := obj.Attrs[create]; !ok { - diags.AddError( - "Create Timeout Not Found", - "Create timeout is not present within the timeouts") - return nil, diags +// 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 { + if _, ok := obj.Attrs[attributeNameCreate]; !ok { + return def } - createTimeout := obj.Attrs[create] + createTimeout := obj.Attrs[attributeNameCreate] + + if createTimeout.IsNull() { + return def + } // Although the schema mutation functions guarantee that the type for create timeout // is a string, this function accepts any types.Object. if _, ok := createTimeout.(types.String); !ok { - diags.AddError( - "Create Timeout Not String", - "Create timeout must be a string") - return nil, diags + 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(createTimeout.(types.String).Value) if err != nil { - diags.AddError( - "Create Timeout Not Parseable", - "Create timeout cannot be parsed as time.Duration") - return nil, diags + return def } - return &duration, nil + return duration } -// CreateDefault returns time.Duration generated from parsing the "create" value in obj.Attrs -// or the supplied default if "create" cannot be found or parsed. -func CreateDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - duration, diags := Create(ctx, obj) - - if diags.HasError() { +// 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 { + if _, ok := obj.Attrs[attributeNameRead]; !ok { return def } - return *duration -} + readTimeout := obj.Attrs[attributeNameRead] -// 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. -func Read(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { - var diags diag.Diagnostics - - if _, ok := obj.Attrs[read]; !ok { - diags.AddError( - "Read Timeout Not Found", - "Read timeout is not present within the timeouts") - return nil, diags + if readTimeout.IsNull() { + return def } - readTimeout := obj.Attrs[read] - // Although the schema mutation functions guarantee that the type for read timeout // is a string, this function accepts any types.Object. if _, ok := readTimeout.(types.String); !ok { - diags.AddError( - "Read Timeout Not String", - "Read timeout must be a string") - return nil, diags + 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(readTimeout.(types.String).Value) if err != nil { - diags.AddError( - "Read Timeout Not Parseable", - "Read timeout cannot be parsed as time.Duration") - return nil, diags + return def } - return &duration, nil + return duration } -// ReadDefault returns time.Duration generated from parsing the "read" value in obj.Attrs -// or the supplied default if "read" cannot be found or parsed. -func ReadDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - duration, diags := Read(ctx, obj) - - if diags.HasError() { +// 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 { + if _, ok := obj.Attrs[attributeNameUpdate]; !ok { 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. -func Update(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { - var diags diag.Diagnostics + updateTimeout := obj.Attrs[attributeNameUpdate] - if _, ok := obj.Attrs[update]; !ok { - diags.AddError( - "Update Timeout Not Found", - "Update timeout is not present within the timeouts") - return nil, diags + if updateTimeout.IsNull() { + return def } - updateTimeout := obj.Attrs[update] - // Although the schema mutation functions guarantee that the type for update timeout // is a string, this function accepts any types.Object. if _, ok := updateTimeout.(types.String); !ok { - diags.AddError( - "Update Timeout Not String", - "Update timeout must be a string") - return nil, diags + 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(updateTimeout.(types.String).Value) if err != nil { - diags.AddError( - "Update Timeout Not Parseable", - "Update timeout cannot be parsed as time.Duration") - return nil, diags + return def } - return &duration, nil + return duration } -// UpdateDefault returns time.Duration generated from parsing the "update" value in obj.Attrs -// or the supplied default if "update" cannot be found or parsed. -func UpdateDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - duration, diags := Update(ctx, obj) - - if diags.HasError() { +// 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 { + if _, ok := obj.Attrs[attributeNameDelete]; !ok { return def } - return *duration -} + deleteTimeout := obj.Attrs[attributeNameDelete] -// 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. -func Delete(ctx context.Context, obj types.Object) (*time.Duration, diag.Diagnostics) { - var diags diag.Diagnostics - - if _, ok := obj.Attrs[del]; !ok { - diags.AddError( - "Delete Timeout Not Found", - "Delete timeout is not present within the timeouts") - return nil, diags + if deleteTimeout.IsNull() { + return def } - deleteTimeout := obj.Attrs[del] - // Although the schema mutation functions guarantee that the type for delete timeout // is a string, this function accepts any types.Object. if _, ok := deleteTimeout.(types.String); !ok { - diags.AddError( - "Delete Timeout Not String", - "Delete timeout must be a string") - return nil, diags + 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(deleteTimeout.(types.String).Value) if err != nil { - diags.AddError( - "Delete Timeout Not Parseable", - "Delete timeout cannot be parsed as time.Duration") - return nil, diags - } - - return &duration, nil -} - -// DeleteDefault returns time.Duration generated from parsing the "delete" value in obj.Attrs -// or the supplied default if "delete" cannot be found or parsed. -func DeleteDefault(ctx context.Context, obj types.Object, def time.Duration) time.Duration { - duration, diags := Delete(ctx, obj) - - if diags.HasError() { return def } - return *duration + return duration } diff --git a/timeouts/parser_test.go b/timeouts/parser_test.go index 257843c..b19c932 100644 --- a/timeouts/parser_test.go +++ b/timeouts/parser_test.go @@ -7,28 +7,36 @@ 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-framework-timeouts/timeouts" ) -func TestType_Create(t *testing.T) { +func TestCreate(t *testing.T) { t.Parallel() + def := 20 * time.Minute + type testCase struct { - obj types.Object - expected *time.Duration - expectedDiag diag.Diagnostic + obj types.Object + expected time.Duration } tests := map[string]testCase{ "create-not-present": { - obj: types.Object{}, - expectedDiag: diag.NewErrorDiagnostic( - "Create Timeout Not Found", - "Create timeout is not present within the timeouts", - ), + obj: types.Object{}, + expected: def, + }, + "create-is-null": { + obj: types.Object{ + Attrs: map[string]attr.Value{ + "create": types.String{Null: true}, + }, + AttrTypes: map[string]attr.Type{ + "create": types.StringType, + }, + }, + expected: def, }, "create-not-string": { obj: types.Object{ @@ -39,10 +47,7 @@ func TestType_Create(t *testing.T) { "create": types.BoolType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Create Timeout Not String", - "Create timeout must be a string", - ), + expected: def, }, "create-not-parseable-empty": { obj: types.Object{ @@ -55,10 +60,7 @@ func TestType_Create(t *testing.T) { "create": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Create Timeout Not Parseable", - "Create timeout cannot be parsed as time.Duration", - ), + expected: def, }, "create-not-parseable": { obj: types.Object{ @@ -71,10 +73,7 @@ func TestType_Create(t *testing.T) { "create": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Create Timeout Not Parseable", - "Create timeout cannot be parsed as time.Duration", - ), + expected: def, }, "create-valid": { obj: types.Object{ @@ -87,33 +86,26 @@ func TestType_Create(t *testing.T) { "create": types.StringType, }, }, - expected: ptr(60 * time.Minute), + expected: 60 * time.Minute, }, } for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - expectedDiags := diag.Diagnostics{} - expectedDiags.Append(test.expectedDiag) - - actual, diags := timeouts.Create(context.Background(), test.obj) + actual := timeouts.Create(context.Background(), test.obj, def) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected duration difference: %s", diff) } - - if diff := cmp.Diff(diags, expectedDiags); diff != "" { - t.Errorf("unexpected diags difference: %s", diff) - } }) } } -func TestType_CreateDefault(t *testing.T) { +func TestRead(t *testing.T) { t.Parallel() - defaultTimeout := 20 * time.Minute + def := 20 * time.Minute type testCase struct { obj types.Object @@ -121,78 +113,20 @@ func TestType_CreateDefault(t *testing.T) { } tests := map[string]testCase{ - "create-not-present": { - expected: defaultTimeout, - }, - "create-not-parseable-empty": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "create": types.String{ - Value: "", - }, - }, - AttrTypes: map[string]attr.Type{ - "create": types.StringType, - }, - }, - expected: defaultTimeout, - }, - "create-not-parseable": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "create": types.String{ - Value: "60x", - }, - }, - AttrTypes: map[string]attr.Type{ - "create": types.StringType, - }, - }, - expected: defaultTimeout, + "read-not-present": { + obj: types.Object{}, + expected: def, }, - "create-valid": { + "read-is-null": { obj: types.Object{ Attrs: map[string]attr.Value{ - "create": types.String{ - Value: "60m", - }, + "read": types.String{Null: true}, }, AttrTypes: map[string]attr.Type{ - "create": types.StringType, + "read": types.StringType, }, }, - expected: 60 * time.Minute, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.CreateDefault(context.Background(), test.obj, defaultTimeout) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - }) - } -} - -func TestType_Read(t *testing.T) { - t.Parallel() - - type testCase struct { - obj types.Object - expected *time.Duration - expectedDiag diag.Diagnostic - } - - tests := map[string]testCase{ - "read-not-present": { - obj: types.Object{}, - expectedDiag: diag.NewErrorDiagnostic( - "Read Timeout Not Found", - "Read timeout is not present within the timeouts", - ), + expected: def, }, "read-not-string": { obj: types.Object{ @@ -203,10 +137,7 @@ func TestType_Read(t *testing.T) { "read": types.BoolType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Read Timeout Not String", - "Read timeout must be a string", - ), + expected: def, }, "read-not-parseable-empty": { obj: types.Object{ @@ -219,10 +150,7 @@ func TestType_Read(t *testing.T) { "read": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Read Timeout Not Parseable", - "Read timeout cannot be parsed as time.Duration", - ), + expected: def, }, "read-not-parseable": { obj: types.Object{ @@ -235,10 +163,7 @@ func TestType_Read(t *testing.T) { "read": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Read Timeout Not Parseable", - "Read timeout cannot be parsed as time.Duration", - ), + expected: def, }, "read-valid": { obj: types.Object{ @@ -251,33 +176,26 @@ func TestType_Read(t *testing.T) { "read": types.StringType, }, }, - expected: ptr(60 * time.Minute), + expected: 60 * time.Minute, }, } for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - expectedDiags := diag.Diagnostics{} - expectedDiags.Append(test.expectedDiag) - - actual, diags := timeouts.Read(context.Background(), test.obj) + actual := timeouts.Read(context.Background(), test.obj, def) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected duration difference: %s", diff) } - - if diff := cmp.Diff(diags, expectedDiags); diff != "" { - t.Errorf("unexpected diags difference: %s", diff) - } }) } } -func TestType_ReadDefault(t *testing.T) { +func TestUpdate(t *testing.T) { t.Parallel() - defaultTimeout := 20 * time.Minute + def := 20 * time.Minute type testCase struct { obj types.Object @@ -285,78 +203,20 @@ func TestType_ReadDefault(t *testing.T) { } tests := map[string]testCase{ - "read-not-present": { - expected: defaultTimeout, - }, - "read-not-parseable-empty": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "read": types.String{ - Value: "", - }, - }, - AttrTypes: map[string]attr.Type{ - "read": types.StringType, - }, - }, - expected: defaultTimeout, - }, - "read-not-parseable": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "read": types.String{ - Value: "60x", - }, - }, - AttrTypes: map[string]attr.Type{ - "read": types.StringType, - }, - }, - expected: defaultTimeout, + "update-not-present": { + obj: types.Object{}, + expected: def, }, - "read-valid": { + "update-is-null": { obj: types.Object{ Attrs: map[string]attr.Value{ - "read": types.String{ - Value: "60m", - }, + "update": types.String{Null: true}, }, AttrTypes: map[string]attr.Type{ - "read": types.StringType, + "update": types.StringType, }, }, - expected: 60 * time.Minute, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.ReadDefault(context.Background(), test.obj, defaultTimeout) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - }) - } -} - -func TestType_Update(t *testing.T) { - t.Parallel() - - type testCase struct { - obj types.Object - expected *time.Duration - expectedDiag diag.Diagnostic - } - - tests := map[string]testCase{ - "update-not-present": { - obj: types.Object{}, - expectedDiag: diag.NewErrorDiagnostic( - "Update Timeout Not Found", - "Update timeout is not present within the timeouts", - ), + expected: def, }, "update-not-string": { obj: types.Object{ @@ -367,10 +227,7 @@ func TestType_Update(t *testing.T) { "update": types.BoolType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Update Timeout Not String", - "Update timeout must be a string", - ), + expected: def, }, "update-not-parseable-empty": { obj: types.Object{ @@ -383,10 +240,7 @@ func TestType_Update(t *testing.T) { "update": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Update Timeout Not Parseable", - "Update timeout cannot be parsed as time.Duration", - ), + expected: def, }, "update-not-parseable": { obj: types.Object{ @@ -399,10 +253,7 @@ func TestType_Update(t *testing.T) { "update": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Update Timeout Not Parseable", - "Update timeout cannot be parsed as time.Duration", - ), + expected: def, }, "update-valid": { obj: types.Object{ @@ -415,33 +266,26 @@ func TestType_Update(t *testing.T) { "update": types.StringType, }, }, - expected: ptr(60 * time.Minute), + expected: 60 * time.Minute, }, } for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - expectedDiags := diag.Diagnostics{} - expectedDiags.Append(test.expectedDiag) - - actual, diags := timeouts.Update(context.Background(), test.obj) + actual := timeouts.Update(context.Background(), test.obj, def) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected duration difference: %s", diff) } - - if diff := cmp.Diff(diags, expectedDiags); diff != "" { - t.Errorf("unexpected diags difference: %s", diff) - } }) } } -func TestType_UpdateDefault(t *testing.T) { +func TestDelete(t *testing.T) { t.Parallel() - defaultTimeout := 20 * time.Minute + def := 20 * time.Minute type testCase struct { obj types.Object @@ -449,78 +293,20 @@ func TestType_UpdateDefault(t *testing.T) { } tests := map[string]testCase{ - "update-not-present": { - expected: defaultTimeout, - }, - "update-not-parseable-empty": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "update": types.String{ - Value: "", - }, - }, - AttrTypes: map[string]attr.Type{ - "update": types.StringType, - }, - }, - expected: defaultTimeout, + "delete-not-present": { + obj: types.Object{}, + expected: def, }, - "update-not-parseable": { + "delete-is-null": { obj: types.Object{ Attrs: map[string]attr.Value{ - "update": types.String{ - Value: "60x", - }, + "delete": types.String{Null: true}, }, AttrTypes: map[string]attr.Type{ - "update": types.StringType, - }, - }, - expected: defaultTimeout, - }, - "update-valid": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "update": types.String{ - Value: "60m", - }, - }, - AttrTypes: map[string]attr.Type{ - "update": types.StringType, + "delete": types.StringType, }, }, - expected: 60 * time.Minute, - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - actual := timeouts.UpdateDefault(context.Background(), test.obj, defaultTimeout) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - }) - } -} - -func TestType_Delete(t *testing.T) { - t.Parallel() - - type testCase struct { - obj types.Object - expected *time.Duration - expectedDiag diag.Diagnostic - } - - tests := map[string]testCase{ - "delete-not-present": { - obj: types.Object{}, - expectedDiag: diag.NewErrorDiagnostic( - "Delete Timeout Not Found", - "Delete timeout is not present within the timeouts", - ), + expected: def, }, "delete-not-string": { obj: types.Object{ @@ -531,10 +317,7 @@ func TestType_Delete(t *testing.T) { "delete": types.BoolType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Delete Timeout Not String", - "Delete timeout must be a string", - ), + expected: def, }, "delete-not-parseable-empty": { obj: types.Object{ @@ -547,10 +330,7 @@ func TestType_Delete(t *testing.T) { "delete": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Delete Timeout Not Parseable", - "Delete timeout cannot be parsed as time.Duration", - ), + expected: def, }, "delete-not-parseable": { obj: types.Object{ @@ -563,84 +343,7 @@ func TestType_Delete(t *testing.T) { "delete": types.StringType, }, }, - expectedDiag: diag.NewErrorDiagnostic( - "Delete Timeout Not Parseable", - "Delete timeout cannot be parsed as time.Duration", - ), - }, - "delete-valid": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "delete": types.String{ - Value: "60m", - }, - }, - AttrTypes: map[string]attr.Type{ - "delete": types.StringType, - }, - }, - expected: ptr(60 * time.Minute), - }, - } - - for name, test := range tests { - name, test := name, test - t.Run(name, func(t *testing.T) { - expectedDiags := diag.Diagnostics{} - expectedDiags.Append(test.expectedDiag) - - actual, diags := timeouts.Delete(context.Background(), test.obj) - - if diff := cmp.Diff(actual, test.expected); diff != "" { - t.Errorf("unexpected duration difference: %s", diff) - } - - if diff := cmp.Diff(diags, expectedDiags); diff != "" { - t.Errorf("unexpected diags difference: %s", diff) - } - }) - } -} - -func TestType_DeleteDefault(t *testing.T) { - t.Parallel() - - defaultTimeout := 20 * time.Minute - - type testCase struct { - obj types.Object - expected time.Duration - } - - tests := map[string]testCase{ - "delete-not-present": { - expected: defaultTimeout, - }, - "delete-not-parseable-empty": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "delete": types.String{ - Value: "", - }, - }, - AttrTypes: map[string]attr.Type{ - "delete": types.StringType, - }, - }, - expected: defaultTimeout, - }, - "delete-not-parseable": { - obj: types.Object{ - Attrs: map[string]attr.Value{ - "delete": types.String{ - Value: "60x", - }, - }, - AttrTypes: map[string]attr.Type{ - "delete": types.StringType, - }, - }, - expected: defaultTimeout, + expected: def, }, "delete-valid": { obj: types.Object{ @@ -660,7 +363,7 @@ func TestType_DeleteDefault(t *testing.T) { for name, test := range tests { name, test := name, test t.Run(name, func(t *testing.T) { - actual := timeouts.DeleteDefault(context.Background(), test.obj, defaultTimeout) + actual := timeouts.Delete(context.Background(), test.obj, def) if diff := cmp.Diff(actual, test.expected); diff != "" { t.Errorf("unexpected duration difference: %s", diff) @@ -668,7 +371,3 @@ func TestType_DeleteDefault(t *testing.T) { }) } } - -func ptr[T any](v T) *T { - return &v -} diff --git a/timeouts/schema.go b/timeouts/schema.go index 69b6c20..bfccf25 100644 --- a/timeouts/schema.go +++ b/timeouts/schema.go @@ -10,10 +10,10 @@ import ( ) const ( - create = "create" - read = "read" - update = "update" - del = "delete" + attributeNameCreate = "create" + attributeNameRead = "read" + attributeNameUpdate = "update" + attributeNameDelete = "delete" ) // Opts is used as an argument to Block and Attributes to indicate which attributes @@ -84,19 +84,19 @@ func attributesMap(opts Opts) map[string]tfsdk.Attribute { } if opts.Create { - attributes[create] = attribute + attributes[attributeNameCreate] = attribute } if opts.Read { - attributes[read] = attribute + attributes[attributeNameRead] = attribute } if opts.Update { - attributes[update] = attribute + attributes[attributeNameUpdate] = attribute } if opts.Delete { - attributes[del] = attribute + attributes[attributeNameDelete] = attribute } return attributes From 055bce5ff33ebd2428d8c1ed1e168176e5d60b52 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 21 Sep 2022 09:57:15 +0100 Subject: [PATCH 14/15] Updating README (#4) --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 89dd492..d783314 100644 --- a/README.md +++ b/README.md @@ -126,17 +126,14 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, } defaultCreateTimeout := 20 * time.Minutes + createTimeout, diags := timeouts.Create(ctx, data.Timeouts, defaultCreateTimeout) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if createTimeout == nil { - /* ... */ - } - - ctx, cancel := context.WithTimeout(ctx, *createTimeout) + ctx, cancel := context.WithTimeout(ctx, createTimeout) defer cancel() /* ... */ From adb64752daf7a550342733314f4ab80b1b1f5412 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Wed, 21 Sep 2022 10:03:02 +0100 Subject: [PATCH 15/15] Updating README (#4) --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index d783314..59e9ad5 100644 --- a/README.md +++ b/README.md @@ -127,11 +127,7 @@ func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, defaultCreateTimeout := 20 * time.Minutes - createTimeout, diags := timeouts.Create(ctx, data.Timeouts, defaultCreateTimeout) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } + createTimeout := timeouts.Create(ctx, data.Timeouts, defaultCreateTimeout) ctx, cancel := context.WithTimeout(ctx, createTimeout) defer cancel()