Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/data-sources/cdn_custom_domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ data "stackit_cdn_custom_domain" "example" {

Read-Only:

- `skip_dns_check` (Boolean) When true, skips the verification check that the custom domain points to the distribution domain via CNAME. Useful for zero-downtime migrations.
- `version` (Number) A version identifier for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.
6 changes: 4 additions & 2 deletions docs/resources/cdn_custom_domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ resource "stackit_cdn_custom_domain" "example" {
distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "https://xxx.xxx"
certificate = {
certificate = "-----BEGIN CERTIFICATE-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END CERTIFICATE---"
private_key = "-----BEGIN RSA PRIVATE KEY-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END RSA PRIVATE KEY---"
certificate = "-----BEGIN CERTIFICATE-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END CERTIFICATE---"
private_key = "-----BEGIN RSA PRIVATE KEY-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END RSA PRIVATE KEY---"
skip_dns_check = true
}
}
```
Expand Down Expand Up @@ -53,6 +54,7 @@ Optional:

- `certificate` (String, Sensitive) The PEM-encoded TLS certificate. Required for custom certificates.
- `private_key` (String, Sensitive) The PEM-encoded private key for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.
- `skip_dns_check` (Boolean) When true, skips the verification check that the custom domain points to the distribution domain via CNAME. Useful for zero-downtime migrations.

Read-Only:

Expand Down
5 changes: 3 additions & 2 deletions examples/resources/stackit_cdn_custom_domain/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ resource "stackit_cdn_custom_domain" "example" {
distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "https://xxx.xxx"
certificate = {
certificate = "-----BEGIN CERTIFICATE-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END CERTIFICATE---"
private_key = "-----BEGIN RSA PRIVATE KEY-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END RSA PRIVATE KEY---"
certificate = "-----BEGIN CERTIFICATE-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END CERTIFICATE---"
private_key = "-----BEGIN RSA PRIVATE KEY-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END RSA PRIVATE KEY---"
skip_dns_check = true
}
}
3 changes: 3 additions & 0 deletions stackit/internal/services/cdn/cdn_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ func TestAccCDNDistributionHttp(t *testing.T) {
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainNameHttp),
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"),
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.skip_dns_check", "false"),
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"),
),
Expand Down Expand Up @@ -396,6 +397,7 @@ func TestAccCDNDistributionHttp(t *testing.T) {
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", fullDomainNameHttp),
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"),
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "certificate.skip_dns_check", "false"),
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
),
},
Expand Down Expand Up @@ -456,6 +458,7 @@ func TestAccCDNDistributionHttp(t *testing.T) {
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainNameHttp),
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"),
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.skip_dns_check", "false"),
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"),
),
Expand Down
16 changes: 13 additions & 3 deletions stackit/internal/services/cdn/customdomain/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ var (
)

var certificateDataSourceTypes = map[string]attr.Type{
"version": types.Int32Type,
"version": types.Int32Type,
"skip_dns_check": types.BoolType,
}

type customDomainDataSource struct {
Expand Down Expand Up @@ -112,6 +113,10 @@ func (r *customDomainDataSource) Schema(_ context.Context, _ datasource.SchemaRe
Description: certificateSchemaDescriptions["version"],
Computed: true,
},
"skip_dns_check": schema.BoolAttribute{
Description: certificateSchemaDescriptions["skip_dns_check"],
Computed: true,
},
},
},
},
Expand Down Expand Up @@ -191,14 +196,19 @@ func mapCustomDomainDataSourceFields(customDomainResponse *cdnSdk.GetCustomDomai
if normalizedCert.Type == "managed" {
model.Certificate = types.ObjectNull(certificateDataSourceTypes)
} else {
// For custom certificates, we only care about the version.
// For custom certificates, we only care about the version and skip_dns_check.
version := types.Int32Null()
if normalizedCert.Version != nil {
version = types.Int32Value(*normalizedCert.Version)
}
skipDnsCheck := types.BoolNull()
if normalizedCert.SkipDnsCheck != nil {
skipDnsCheck = types.BoolValue(*normalizedCert.SkipDnsCheck)
}

certificateObj, diags := types.ObjectValue(certificateDataSourceTypes, map[string]attr.Value{
"version": version,
"version": version,
"skip_dns_check": skipDnsCheck,
})
if diags.HasError() {
return fmt.Errorf("failed to map certificate: %w", core.DiagsToError(diags))
Expand Down
26 changes: 23 additions & 3 deletions stackit/internal/services/cdn/customdomain/datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ func TestMapDataSourceFields(t *testing.T) {

// Expected certificate object when a custom certificate is returned
certAttributes := map[string]attr.Value{
"version": types.Int32Value(3),
"version": types.Int32Value(3),
"skip_dns_check": types.BoolValue(false),
}
certificateObj, _ := types.ObjectValue(certificateDataSourceTypes, certAttributes)

Expand All @@ -40,8 +41,9 @@ func TestMapDataSourceFields(t *testing.T) {
customVersion := int32(3)
getRespCustom := cdnSdk.GetCustomDomainResponseCertificate{
GetCustomDomainCustomCertificate: &cdnSdk.GetCustomDomainCustomCertificate{
Type: customType,
Version: customVersion,
Type: customType,
Version: customVersion,
SkipDnsCheck: false,
},
}

Expand Down Expand Up @@ -83,6 +85,24 @@ func TestMapDataSourceFields(t *testing.T) {
Input: customDomainFixture(),
IsValid: true,
},
"happy_path_custom_cert_skip_dns_check_true": {
Expected: expectedModel(func(m *customDomainDataSourceModel) {
m.Certificate = types.ObjectValueMust(certificateDataSourceTypes, map[string]attr.Value{
"version": types.Int32Value(3),
"skip_dns_check": types.BoolValue(true),
})
}),
Input: customDomainFixture(func(gcdr *cdnSdk.GetCustomDomainResponse) {
gcdr.Certificate = cdnSdk.GetCustomDomainResponseCertificate{
GetCustomDomainCustomCertificate: &cdnSdk.GetCustomDomainCustomCertificate{
Type: customType,
Version: customVersion,
SkipDnsCheck: true,
},
}
}),
IsValid: true,
},
"happy_path_managed_cert": {
Expected: expectedModel(func(m *customDomainDataSourceModel) {
m.Certificate = types.ObjectNull(certificateDataSourceTypes)
Expand Down
54 changes: 36 additions & 18 deletions stackit/internal/services/cdn/customdomain/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,18 @@ var (
_ resource.ResourceWithImportState = &customDomainResource{}
)
var certificateSchemaDescriptions = map[string]string{
"main": "The TLS certificate for the custom domain. If omitted, a managed certificate will be used. If the block is specified, a custom certificate is used.",
"certificate": "The PEM-encoded TLS certificate. Required for custom certificates.",
"private_key": "The PEM-encoded private key for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.",
"version": "A version identifier for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.",
"main": "The TLS certificate for the custom domain. If omitted, a managed certificate will be used. If the block is specified, a custom certificate is used.",
"certificate": "The PEM-encoded TLS certificate. Required for custom certificates.",
"private_key": "The PEM-encoded private key for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.",
"version": "A version identifier for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.",
"skip_dns_check": "When true, skips the verification check that the custom domain points to the distribution domain via CNAME. Useful for zero-downtime migrations.",
}

var certificateTypes = map[string]attr.Type{
"version": types.Int32Type,
"certificate": types.StringType,
"private_key": types.StringType,
"version": types.Int32Type,
"certificate": types.StringType,
"private_key": types.StringType,
"skip_dns_check": types.BoolType,
}

var customDomainSchemaDescriptions = map[string]string{
Expand All @@ -63,9 +65,10 @@ var customDomainSchemaDescriptions = map[string]string{
}

type CertificateModel struct {
Certificate types.String `tfsdk:"certificate"`
PrivateKey types.String `tfsdk:"private_key"`
Version types.Int32 `tfsdk:"version"`
Certificate types.String `tfsdk:"certificate"`
PrivateKey types.String `tfsdk:"private_key"`
Version types.Int32 `tfsdk:"version"`
SkipDnsCheck types.Bool `tfsdk:"skip_dns_check"`
}

type CustomDomainModel struct {
Expand All @@ -87,8 +90,9 @@ func NewCustomDomainResource() resource.Resource {
}

type Certificate struct {
Type string
Version *int32
Type string
Version *int32
SkipDnsCheck *bool
}

func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
Expand Down Expand Up @@ -166,6 +170,11 @@ func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaReques
Description: certificateSchemaDescriptions["version"],
Computed: true,
},
"skip_dns_check": schema.BoolAttribute{
Description: certificateSchemaDescriptions["skip_dns_check"],
Optional: true,
Computed: true,
Comment on lines +175 to +176

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting optional and computed to true can lead to unexpected behavior. At the moment it has the following behavior:

  1. When the custom domain is created without configuring skip_dns_check -> the API sets it on default to false
  2. After the creation the skip_dns_check will be set to true -> skip_dns_check is true
  3. In an additional update the configuration for skip_dns_check will be removed -> Nothing happens. It remains in the last state (independent if it was set to false or true before). I would expect that it will be set to false again like it was in the first step without any configuration

},
},
},
"status": schema.StringAttribute{
Expand Down Expand Up @@ -417,8 +426,9 @@ func normalizeCertificate(certInput cdnSdk.GetCustomDomainResponseCertificate) (
// Now we process the extracted certificates
if customCert != nil && customCert.Type != "" {
return Certificate{
Type: customCert.Type,
Version: new(customCert.Version),
Type: customCert.Type,
Version: &customCert.Version,
SkipDnsCheck: &customCert.SkipDnsCheck,
}, nil
}

Expand Down Expand Up @@ -465,6 +475,7 @@ func toCertificatePayload(ctx context.Context, model *CustomDomainModel) (*cdnSd
keyStr,
"custom",
)
customCert.SkipDnsCheck = conversion.BoolValueToPointer(certModel.SkipDnsCheck)
certPayload := cdnSdk.PutCustomDomainCustomCertificateAsPutCustomDomainPayloadCertificate(customCert)

return &certPayload, nil
Expand Down Expand Up @@ -495,11 +506,12 @@ func mapCustomDomainResourceFields(customDomainResponse *cdnSdk.GetCustomDomainR
model.Certificate = types.ObjectNull(certificateTypes)
} else {
// If the certificate is custom, we need to preserve the user-configured
// certificate and private key from the plan/state, and only update the computed version.
// certificate and private key from the plan/state, and update the computed version and skip_dns_check.
certAttributes := map[string]attr.Value{
"certificate": types.StringNull(), // Default to null
"private_key": types.StringNull(), // Default to null
"version": types.Int32Null(),
"certificate": types.StringNull(), // Default to null
"private_key": types.StringNull(), // Default to null
"version": types.Int32Null(),
"skip_dns_check": types.BoolNull(),
}

// Get existing values from the model's certificate object if it exists
Expand All @@ -511,12 +523,18 @@ func mapCustomDomainResourceFields(customDomainResponse *cdnSdk.GetCustomDomainR
if val, ok := existingAttrs["private_key"]; ok {
certAttributes["private_key"] = val
}
if val, ok := existingAttrs["skip_dns_check"]; ok {
certAttributes["skip_dns_check"] = val
}
Comment on lines +526 to +528

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary here. Within the if clause !model.Certificate.IsNull(), only attributes which are not returned by the API are mapped to certAttributes[""]. But skip_dns_check will be returned by the API and because of it, the actual value from the API should be written to certAttributes (what is already done a few lines below). Otherwise potentially state drifts will not be recognized, if the values in the API has changed.

}

// Set the computed version from the API response
if normalizedCert.Version != nil {
certAttributes["version"] = types.Int32Value(*normalizedCert.Version)
}
if normalizedCert.SkipDnsCheck != nil {
certAttributes["skip_dns_check"] = types.BoolValue(*normalizedCert.SkipDnsCheck)
}
Comment on lines +535 to +537

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be simplified

Suggested change
if normalizedCert.SkipDnsCheck != nil {
certAttributes["skip_dns_check"] = types.BoolValue(*normalizedCert.SkipDnsCheck)
}
certAttributes["skip_dns_check"] = types.BoolPointerValue(normalizedCert.SkipDnsCheck)


certificateObj, diags := types.ObjectValue(certificateTypes, certAttributes)
if diags.HasError() {
Expand Down
Loading