Skip to content

Commit

Permalink
Add certificate timeouts (#185)
Browse files Browse the repository at this point in the history
Adds read timeout with retry function to dnsimple_certificate data source.

- Timout can be set in the plan data source definition and defaults to 5 minutes.
- Retry checks are on the certificate state and run every 20 seconds.
- During testing, certificates were issued between 40 seconds and 5 minutes after submitting the order.
  • Loading branch information
AGS4NO authored Mar 13, 2024
1 parent f5209a5 commit 36d619e
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 28 deletions.
8 changes: 8 additions & 0 deletions docs/data-sources/certificate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<a id="nestedblock--timeouts"></a>

### Nested Schema for `timeouts`

Optional:

- `read` (String) - The timeout for the read operation e.g. `5m`
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
10 changes: 9 additions & 1 deletion internal/consts/provider.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
142 changes: 115 additions & 27 deletions internal/framework/datasources/certificate_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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),
},
}
}

Expand All @@ -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)...)
Expand All @@ -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
}

0 comments on commit 36d619e

Please sign in to comment.