From 4d1db91feab1ccfae9698d236556303f5464d025 Mon Sep 17 00:00:00 2001 From: Thomas Gerbet Date: Mon, 22 Apr 2024 17:11:05 +0200 Subject: [PATCH] feat: Add a `wait_for` attribute to the `mailjet_sender_validate` resource The validation does not always succeed on the first try even when all the resources exists. This attribute adds the possibility to let the provider retry multiple times before failing. --- .github/workflows/CI.yml | 7 ++ docs/resources/sender_validate.md | 4 + go.mod | 1 + mailjet/sender_validate_resource.go | 42 ++++++++++- mailjet/string_timeduration_validator.go | 55 ++++++++++++++ mailjet/string_timeduration_validator_test.go | 73 +++++++++++++++++++ 6 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 mailjet/string_timeduration_validator.go create mode 100644 mailjet/string_timeduration_validator_test.go diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 56d0f75..6b8bb68 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -21,6 +21,13 @@ jobs: - uses: cachix/install-nix-action@7ac1ec25491415c381d9b62f0657c7a028df52a7 - run: nix-shell --run 'go build' - run: ./terraform-provider-mailjet --help + unit_tests: + runs-on: ubuntu-22.04 + name: Run unit tests + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: cachix/install-nix-action@7ac1ec25491415c381d9b62f0657c7a028df52a7 + - run: nix-shell --run 'go test -v ./mailjet' static_analysis: runs-on: ubuntu-22.04 name: Run static analysis and linting diff --git a/docs/resources/sender_validate.md b/docs/resources/sender_validate.md index 4785448..7cb8396 100644 --- a/docs/resources/sender_validate.md +++ b/docs/resources/sender_validate.md @@ -33,3 +33,7 @@ resource "mailjet_sender_validate" "sender_validate_example" { ### Required - `id` (Number) Unique numeric ID for the sender you want to validate. + +### Optional + +- `wait_for` (String) When specified, the provider will make multiple attempts to validate the resource until the specified duration is reached. One attempt is made per second. diff --git a/go.mod b/go.mod index 69390e2..8d50c15 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/enalean/terraform-provider-mailjet go 1.22 require ( + github.com/google/go-cmp v0.6.0 github.com/hashicorp/terraform-plugin-framework v1.8.0 github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0 github.com/mailjet/mailjet-apiv3-go/v4 v4.0.1 diff --git a/mailjet/sender_validate_resource.go b/mailjet/sender_validate_resource.go index ef3606d..d6178b8 100644 --- a/mailjet/sender_validate_resource.go +++ b/mailjet/sender_validate_resource.go @@ -3,11 +3,13 @@ package mailjet import ( "context" "fmt" + "time" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/mailjet/mailjet-apiv3-go/v3/resources" "github.com/mailjet/mailjet-apiv3-go/v4" @@ -58,12 +60,20 @@ func (r *senderValidateResource) Schema(_ context.Context, _ resource.SchemaRequ int64planmodifier.RequiresReplace(), }, }, + "wait_for": schema.StringAttribute{ + Optional: true, + Description: "When specified, the provider will make multiple attempts to validate the resource until the specified duration is reached. One attempt is made per second.", + Validators: []validator.String{ + TimeDurationAtLeast1Sec(), + }, + }, }, } } type senderValidateResourceModel struct { - ID types.Int64 `tfsdk:"id"` + ID types.Int64 `tfsdk:"id"` + WaitFor types.String `tfsdk:"wait_for"` } func (r *senderValidateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -78,8 +88,23 @@ func (r *senderValidateResource) Create(ctx context.Context, req resource.Create Resource: "sender", ID: state.ID.ValueInt64(), } + + durationString := state.WaitFor.ValueString() + if durationString == "" { + durationString = "0s" + } + + waitForDuration, err := time.ParseDuration(durationString) + if err != nil { + resp.Diagnostics.AddError( + "Failed to parse wait_for", + err.Error(), + ) + return + } + var responseDataSearch []resources.Sender - err := r.client.Get(senderSearchRequest, &responseDataSearch) + err = r.client.Get(senderSearchRequest, &responseDataSearch) if err == nil && len(responseDataSearch) == 1 && responseDataSearch[0].Status == "Active" { diags := resp.State.Set(ctx, state) @@ -99,6 +124,19 @@ func (r *senderValidateResource) Create(ctx context.Context, req resource.Create var responseDataValidation []resources.SenderValidate + startAttempt := time.Now() + for { + err = r.client.Post(mailjetValidateFullRequest, responseDataValidation) + + if err == nil && len(responseDataValidation) == 1 && responseDataValidation[0].GlobalError == "" { + return + } + if time.Since(startAttempt) > waitForDuration { + break + } + time.Sleep(time.Second) + } + err = r.client.Post(mailjetValidateFullRequest, responseDataValidation) if err != nil { resp.Diagnostics.AddError( diff --git a/mailjet/string_timeduration_validator.go b/mailjet/string_timeduration_validator.go new file mode 100644 index 0000000..7693807 --- /dev/null +++ b/mailjet/string_timeduration_validator.go @@ -0,0 +1,55 @@ +package mailjet + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.String = timeDurationValidator{} + +type timeDurationValidator struct { +} + +func (validator timeDurationValidator) Description(_ context.Context) string { + return `must be a string representing a duration of at least 1 second` +} + +func (validator timeDurationValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +func (validator timeDurationValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + s := req.ConfigValue + + if s.IsUnknown() || s.IsNull() { + return + } + + duration, err := time.ParseDuration(s.ValueString()) + + if err != nil { + resp.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + req.Path, + "failed to parse the time duration", + fmt.Sprintf("%q %s", s.ValueString(), validator.Description(ctx))), + ) + return + } + + if duration < time.Second { + resp.Diagnostics.Append(diag.NewAttributeErrorDiagnostic( + req.Path, + "the time duration must be at least 1 second", + fmt.Sprintf("%q %s", s.ValueString(), validator.Description(ctx))), + ) + return + } +} + +func TimeDurationAtLeast1Sec() validator.String { + return timeDurationValidator{} +} diff --git a/mailjet/string_timeduration_validator_test.go b/mailjet/string_timeduration_validator_test.go new file mode 100644 index 0000000..da049e8 --- /dev/null +++ b/mailjet/string_timeduration_validator_test.go @@ -0,0 +1,73 @@ +package mailjet + +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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestTimeDuration(t *testing.T) { + t.Parallel() + + type testCase struct { + val types.String + expectedDiagnostics diag.Diagnostics + } + + tests := map[string]testCase{ + "unknown": { + val: types.StringUnknown(), + }, + "null": { + val: types.StringNull(), + }, + "valid": { + val: types.StringValue("30s"), + }, + "invalid": { + val: types.StringValue("30wrong"), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "failed to parse the time duration", + `"30wrong" must be a string representing a duration of at least 1 second`, + ), + }, + }, + "invalid_too_small": { + val: types.StringValue("0s"), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("test"), + "the time duration must be at least 1 second", + `"0s" must be a string representing a duration of at least 1 second`, + ), + }, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + + response := validator.StringResponse{} + + TimeDurationAtLeast1Sec().ValidateString(context.Background(), request, &response) + + if diff := cmp.Diff(response.Diagnostics, test.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +}