Skip to content
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", "true"),
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", "true"),
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", "true"),
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
13 changes: 10 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,16 @@ 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.BoolPointerValue(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
51 changes: 33 additions & 18 deletions stackit/internal/services/cdn/customdomain/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
Expand All @@ -42,16 +43,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 +66,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 +91,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 +171,12 @@ 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 thread
marceljk marked this conversation as resolved.
Default: booldefault.StaticBool(false),
},
},
},
"status": schema.StringAttribute{
Expand Down Expand Up @@ -417,8 +428,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 +477,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 +508,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 @@ -517,6 +531,7 @@ func mapCustomDomainResourceFields(customDomainResponse *cdnSdk.GetCustomDomainR
if normalizedCert.Version != nil {
certAttributes["version"] = types.Int32Value(*normalizedCert.Version)
}
certAttributes["skip_dns_check"] = types.BoolPointerValue(normalizedCert.SkipDnsCheck)

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