From f62b83c3698bc908279a44f740ff09475b03c98b 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 ++ go.mod | 1 + mailjet/sender_validate_resource.go | 43 ++++++++++- mailjet/string_timeduration_validator.go | 58 +++++++++++++++ mailjet/string_timeduration_validator_test.go | 73 +++++++++++++++++++ 5 files changed, 179 insertions(+), 3 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/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..06b14b8 100644 --- a/mailjet/sender_validate_resource.go +++ b/mailjet/sender_validate_resource.go @@ -3,14 +3,15 @@ package mailjet import ( "context" "fmt" - "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" + "time" ) var ( @@ -58,12 +59,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 +87,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(state.WaitFor.ValueString()) + 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 +123,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..93c7445 --- /dev/null +++ b/mailjet/string_timeduration_validator.go @@ -0,0 +1,58 @@ +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 { +} + +// Description describes the validation in plain text formatting. +func (validator timeDurationValidator) Description(_ context.Context) string { + return `must be a string representing a duration of at least 1 second` +} + +// MarkdownDescription describes the validation in Markdown formatting. +func (validator timeDurationValidator) MarkdownDescription(ctx context.Context) string { + return validator.Description(ctx) +} + +// ValidateString performs the validation. +func (validator timeDurationValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + s := req.ConfigValue + + if s.IsUnknown() || s.IsNull() { + return + } + + 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) + } + }) + } +}