diff --git a/docs/data-sources/certificate.md b/docs/data-sources/certificate.md index 35d6ecd6..4fca8c94 100644 --- a/docs/data-sources/certificate.md +++ b/docs/data-sources/certificate.md @@ -30,3 +30,11 @@ The following attributes are exported: * `root_certificate` - The Root Certificate of the issuing CA * `certificate_chain` - A list of certificates that make up the chain * `private_key` - The corresponding Private Key for the SSL Certificate + + + +### Nested Schema for `timeouts` + +Optional: + +- `read` (String) - The timeout for the read operation e.g. `5m` diff --git a/go.mod b/go.mod index 7af44826..5f21960f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/terraform-providers/terraform-provider-dnsimple require ( github.com/dnsimple/dnsimple-go v1.7.0 github.com/hashicorp/terraform-plugin-docs v0.18.0 + github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework v1.6.1 github.com/hashicorp/terraform-plugin-go v0.22.1 github.com/hashicorp/terraform-plugin-log v0.9.0 diff --git a/go.sum b/go.sum index 84249193..865ef3b8 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRy github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= github.com/hashicorp/terraform-plugin-docs v0.18.0 h1:2bINhzXc+yDeAcafurshCrIjtdu1XHn9zZ3ISuEhgpk= github.com/hashicorp/terraform-plugin-docs v0.18.0/go.mod h1:iIUfaJpdUmpi+rI42Kgq+63jAjI8aZVTyxp3Bvk9Hg8= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY= github.com/hashicorp/terraform-plugin-framework v1.6.1 h1:hw2XrmUu8d8jVL52ekxim2IqDc+2Kpekn21xZANARLU= github.com/hashicorp/terraform-plugin-framework v1.6.1/go.mod h1:aJI+n/hBPhz1J+77GdgNfk5svW12y7fmtxe/5L5IuwI= github.com/hashicorp/terraform-plugin-go v0.22.1 h1:iTS7WHNVrn7uhe3cojtvWWn83cm2Z6ryIUDTRO0EV7w= diff --git a/internal/consts/provider.go b/internal/consts/provider.go index cd7e9027..cab6e246 100644 --- a/internal/consts/provider.go +++ b/internal/consts/provider.go @@ -1,7 +1,15 @@ package consts const ( - BaseURLSandbox = "https://api.sandbox.dnsimple.com" + BaseURLSandbox = "https://api.sandbox.dnsimple.com" + + // Certificate states + CertificateStateCancelled = "cancelled" + CertificateStateFailed = "failed" + CertificateStateIssued = "issued" + CertificateStateRefunded = "refunded" + + // Domain states DomainStateRegistered = "registered" DomainStateHosted = "hosted" DomainStateNew = "new" diff --git a/internal/framework/datasources/certificate_data_source.go b/internal/framework/datasources/certificate_data_source.go index b736c9be..9582d6cb 100644 --- a/internal/framework/datasources/certificate_data_source.go +++ b/internal/framework/datasources/certificate_data_source.go @@ -5,10 +5,21 @@ import ( "fmt" "time" + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/terraform-providers/terraform-provider-dnsimple/internal/consts" "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common" + "github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils" +) + +const ( + CertificateConverged = "certificate_converged" + CertificateFailed = "certificate_failed" + CertificateTimeout = "certificate_timeout" ) // Ensure provider defined types fully satisfy framework interfaces. @@ -25,13 +36,14 @@ type CertificateDataSource struct { // CertificateDataSourceModel describes the data source data model. type CertificateDataSourceModel struct { - Id types.String `tfsdk:"id"` - CertificateId types.Int64 `tfsdk:"certificate_id"` - Domain types.String `tfsdk:"domain"` - ServerCertificate types.String `tfsdk:"server_certificate"` - RootCertificate types.String `tfsdk:"root_certificate"` - CertificateChain types.List `tfsdk:"certificate_chain"` - PrivateKey types.String `tfsdk:"private_key"` + Id types.String `tfsdk:"id"` + CertificateId types.Int64 `tfsdk:"certificate_id"` + Domain types.String `tfsdk:"domain"` + ServerCertificate types.String `tfsdk:"server_certificate"` + RootCertificate types.String `tfsdk:"root_certificate"` + CertificateChain types.List `tfsdk:"certificate_chain"` + PrivateKey types.String `tfsdk:"private_key"` + Timeouts timeouts.Value `tfsdk:"timeouts"` } func (d *CertificateDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -71,6 +83,9 @@ func (d *CertificateDataSource) Schema(ctx context.Context, req datasource.Schem Computed: true, }, }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx), + }, } } @@ -95,7 +110,7 @@ func (d *CertificateDataSource) Configure(ctx context.Context, req datasource.Co } func (d *CertificateDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data CertificateDataSourceModel + var data *CertificateDataSourceModel // Read Terraform configuration data into the model resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) @@ -104,38 +119,111 @@ func (d *CertificateDataSource) Read(ctx context.Context, req datasource.ReadReq return } - response, err := d.config.Client.Certificates.DownloadCertificate(ctx, d.config.AccountID, data.Domain.ValueString(), data.CertificateId.ValueInt64()) + convergenceState, err := tryToConvergeCertificate(ctx, data, &resp.Diagnostics, d, data.CertificateId.ValueInt64()) if err != nil { resp.Diagnostics.AddError( - "failed to download DNSimple Certificate", + "failed to get certificate state", err.Error(), ) return } - data.ServerCertificate = types.StringValue(response.Data.ServerCertificate) - data.RootCertificate = types.StringValue(response.Data.RootCertificate) - chain, diag := types.ListValueFrom(ctx, types.StringType, response.Data.IntermediateCertificates) - if err != nil { - resp.Diagnostics.Append(diag...) + if convergenceState == CertificateFailed || convergenceState == CertificateTimeout { + // Response is already populated with the error we can safely return return } - data.CertificateChain = chain - response, err = d.config.Client.Certificates.GetCertificatePrivateKey(ctx, d.config.AccountID, data.Domain.ValueString(), data.CertificateId.ValueInt64()) + if convergenceState == CertificateConverged { + + response, err := d.config.Client.Certificates.DownloadCertificate(ctx, d.config.AccountID, data.Domain.ValueString(), data.CertificateId.ValueInt64()) + + if err != nil { + resp.Diagnostics.AddError( + "failed to download DNSimple Certificate", + err.Error(), + ) + return + } + + data.ServerCertificate = types.StringValue(response.Data.ServerCertificate) + data.RootCertificate = types.StringValue(response.Data.RootCertificate) + chain, diag := types.ListValueFrom(ctx, types.StringType, response.Data.IntermediateCertificates) + if err != nil { + resp.Diagnostics.Append(diag...) + return + } + data.CertificateChain = chain + + response, err = d.config.Client.Certificates.GetCertificatePrivateKey(ctx, d.config.AccountID, data.Domain.ValueString(), data.CertificateId.ValueInt64()) + + if err != nil { + resp.Diagnostics.AddError( + "failed to download DNSimple Certificate private key", + err.Error(), + ) + return + } + + data.PrivateKey = types.StringValue(response.Data.PrivateKey) + data.Id = types.StringValue(time.Now().UTC().String()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + } +} - if err != nil { - resp.Diagnostics.AddError( - "failed to download DNSimple Certificate private key", - err.Error(), - ) - return +func tryToConvergeCertificate(ctx context.Context, data *CertificateDataSourceModel, diagnostics *diag.Diagnostics, d *CertificateDataSource, certificateID int64) (string, error) { + readTimeout, diags := data.Timeouts.Read(ctx, 5*time.Minute) + + diagnostics.Append(diags...) + + if diagnostics.HasError() { + return CertificateFailed, nil } - data.PrivateKey = types.StringValue(response.Data.PrivateKey) - data.Id = types.StringValue(time.Now().UTC().String()) + err := utils.RetryWithTimeout(ctx, func() (error, bool) { + + certificate, err := d.config.Client.Certificates.GetCertificate(ctx, d.config.AccountID, data.Domain.ValueString(), data.CertificateId.ValueInt64()) + + if err != nil { + return err, false + } + + if certificate.Data.State == consts.CertificateStateFailed { + diagnostics.AddError( + fmt.Sprintf("failed to issue certificate: %s", data.Domain.ValueString()), + "certificate order failed, please investigate why this happened. If you need assistance, please contact support at support@dnsimple.com", + ) + return nil, true + } + + if certificate.Data.State == consts.CertificateStateCancelled || certificate.Data.State == consts.CertificateStateRefunded { + diagnostics.AddError( + fmt.Sprintf("failed to issue certificate: %s", data.Domain.ValueString()), + "certificate order failed, please investigate why this happened. If you need assistance, please contact support at support@dnsimple.com", + ) + return nil, true + } + + if certificate.Data.State != consts.CertificateStateIssued { + tflog.Info(ctx, fmt.Sprintf("[RETRYING] Certificate order is not complete, current state: %s", certificate.Data.State)) + + return fmt.Errorf("certificate has not been issued, current state: %s. You can try to run terraform again to try and converge the certificate", certificate.Data.State), false + } + + return nil, false + }, readTimeout, 20*time.Second) + + if diagnostics.HasError() { + // If we have diagnostic errors, we suspended the retry loop because the certificate is in a bad state, and cannot converge. + return CertificateFailed, nil + } + + if err != nil { + // If we have an error, it means the retry loop timed out, and we cannot converge during this run. + return CertificateTimeout, err + } - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + return CertificateConverged, nil }