Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add certificate timeouts #185

Merged
merged 12 commits into from
Mar 13, 2024
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 [email protected]",
)
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 [email protected]",
)
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
}
Loading