From 41a6ff0e1c1fcddfb6216fd4b0092b4d2c0b500c Mon Sep 17 00:00:00 2001 From: Anton Pavlov <15091368+pavlov-tony@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:12:04 +0100 Subject: [PATCH 1/6] Fix cdn backend model mapper --- .../services/cdn/distribution/resource.go | 8 ++++++ .../cdn/distribution/resource_test.go | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index cbd215c82..1a8722852 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -812,6 +812,14 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution } payload := &cdn.CreateDistributionPayload{ + Backend: &cdn.CreateDistributionPayloadBackend{ + HttpBackendCreate: &cdn.HttpBackendCreate{ + OriginUrl: cfg.Backend.HttpBackend.OriginUrl, + OriginRequestHeaders: cfg.Backend.HttpBackend.OriginRequestHeaders, + Geofencing: cfg.Backend.HttpBackend.Geofencing, + Type: cdn.PtrString("http"), + }, + }, IntentId: cdn.PtrString(uuid.NewString()), Regions: cfg.Regions, BlockedCountries: cfg.BlockedCountries, diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index b4b6fd1c7..eec9ddc99 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -62,6 +62,19 @@ func TestToCreatePayload(t *testing.T) { "happy_path": { Input: modelFixture(), Expected: &cdn.CreateDistributionPayload{ + Backend: &cdn.CreateDistributionPayloadBackend{ + HttpBackendCreate: &cdn.HttpBackendCreate{ + OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), + OriginRequestHeaders: &map[string]string{ + "testHeader0": "testHeaderValue0", + "testHeader1": "testHeaderValue1", + }, + Geofencing: &map[string][]string{ + "https://de.mycoolapp.com": []string{"DE", "FR"}, + }, + Type: cdn.PtrString("http"), + }, + }, Regions: &[]cdn.Region{"EU", "US"}, BlockedCountries: &[]string{"XX", "YY", "ZZ"}, }, @@ -77,6 +90,19 @@ func TestToCreatePayload(t *testing.T) { }) }), Expected: &cdn.CreateDistributionPayload{ + Backend: &cdn.CreateDistributionPayloadBackend{ + HttpBackendCreate: &cdn.HttpBackendCreate{ + OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), + OriginRequestHeaders: &map[string]string{ + "testHeader0": "testHeaderValue0", + "testHeader1": "testHeaderValue1", + }, + Geofencing: &map[string][]string{ + "https://de.mycoolapp.com": []string{"DE", "FR"}, + }, + Type: cdn.PtrString("http"), + }, + }, Regions: &[]cdn.Region{"EU", "US"}, Optimizer: cdn.NewOptimizer(true), BlockedCountries: &[]string{"XX", "YY", "ZZ"}, From 55894a97b5d061d8104c0a0d05b9b358c73f90c4 Mon Sep 17 00:00:00 2001 From: Anton Pavlov <15091368+pavlov-tony@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:56:37 +0100 Subject: [PATCH 2/6] Fix terraform state for blocked_countries field in cdn service --- .../internal/services/cdn/distribution/resource.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 1a8722852..c04dddc64 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" "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" @@ -281,8 +282,18 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, "blocked_countries": schema.ListAttribute{ Optional: true, + Computed: true, // Required when using Default Description: schemaDescriptions["config_blocked_countries"], ElementType: types.StringType, + // The API returns an empty list for blocked_countries even if the field is omitted + // (null) in the request. This causes an "inconsistent result" error in Terraform + // because the config is null but the state is []. + // + // By setting a Default value of an empty list, we tell Terraform to treat a missing + // blocked_countries block in the HCL as if the user explicitly defined + // blocked_countries = []. This ensures the config (empty list) matches the + // API response (empty list). + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, }, }, From 0fc5eef0bce4504b8f23e26bcc307e3793b9d7e4 Mon Sep 17 00:00:00 2001 From: Anton Pavlov <15091368+pavlov-tony@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:57:38 +0100 Subject: [PATCH 3/6] Adjust acceptance tests for cdn service --- stackit/internal/services/cdn/cdn_acc_test.go | 281 ++++++++++++------ 1 file changed, 194 insertions(+), 87 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 0dd031a5b..63752cfad 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -1,6 +1,7 @@ package cdn_test import ( + "bytes" "context" cryptoRand "crypto/rand" "crypto/rsa" @@ -12,6 +13,7 @@ import ( "net" "strings" "testing" + "text/template" "time" "github.com/google/uuid" @@ -24,98 +26,150 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "config_backend_type": "http", - "config_backend_origin_url": "https://test-backend-1.cdn-dev.runs.onstackit.cloud", - "config_regions": "\"EU\", \"US\"", - "config_regions_updated": "\"EU\", \"US\", \"ASIA\"", - "blocked_countries": "\"CU\", \"AQ\"", // Do NOT use DE or AT here, because the request might be blocked by bunny at the time of creation - don't lock yourself out - "custom_domain_prefix": uuid.NewString(), // we use a different domain prefix each test run due to inconsistent upstream release of domains, which might impair consecutive test runs - "dns_name": fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]), +// 1. Define the Configuration Struct +type distributionConfig struct { + ProjectId string + BackendType string + OriginURL string + Geofencing map[string][]string + Regions []string + BlockedCountries []string + OptimizerEnabled bool + DNSName string + CustomDomainPrefix string + Cert string + Key string } -func configResources(regions string, geofencingCountries []string) string { - var quotedCountries []string - for _, country := range geofencingCountries { - quotedCountries = append(quotedCountries, fmt.Sprintf(`%q`, country)) - } +// 2. Define the Template +const distributionTmpl = ` +{{ .ProviderConfig }} - geofencingList := strings.Join(quotedCountries, ",") - return fmt.Sprintf(` - %s - - resource "stackit_cdn_distribution" "distribution" { - project_id = "%s" - config = { - backend = { - type = "http" - origin_url = "%s" - geofencing = { - "%s" = [%s] - } - } - regions = [%s] - blocked_countries = [%s] - - optimizer = { - enabled = true - } - } - } - - resource "stackit_dns_zone" "dns_zone" { - project_id = "%s" - name = "cdn_acc_test_zone" - dns_name = "%s" - contact_email = "aa@bb.cc" - type = "primary" - default_ttl = 3600 - } - resource "stackit_dns_record_set" "dns_record" { - project_id = "%s" - zone_id = stackit_dns_zone.dns_zone.zone_id - name = "%s" - type = "CNAME" - records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] - } - `, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], instanceResource["config_backend_origin_url"], geofencingList, - regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"], - testutil.ProjectId, instanceResource["custom_domain_prefix"]) +resource "stackit_cdn_distribution" "distribution" { + project_id = "{{ .ProjectId }}" + config = { + backend = { + type = "{{ .BackendType }}" + origin_url = "{{ .OriginURL }}" + {{- if .Geofencing }} + geofencing = { + {{- range $url, $countries := .Geofencing }} + "{{ $url }}" = {{ $countries | stringList }} + {{- end }} + } + {{- end }} + } + regions = {{ .Regions | stringList }} + + {{- if .BlockedCountries }} + blocked_countries = {{ .BlockedCountries | stringList }} + {{- end }} + + optimizer = { + enabled = {{ .OptimizerEnabled }} + } + } } -func configCustomDomainResources(regions, cert, key string, geofencingCountries []string) string { - return fmt.Sprintf(` - %s - - resource "stackit_cdn_custom_domain" "custom_domain" { - project_id = stackit_cdn_distribution.distribution.project_id - distribution_id = stackit_cdn_distribution.distribution.distribution_id - name = "${stackit_dns_record_set.dns_record.name}.${stackit_dns_zone.dns_zone.dns_name}" - certificate = { - certificate = %q - private_key = %q - } - } -`, configResources(regions, geofencingCountries), cert, key) +resource "stackit_dns_zone" "dns_zone" { + project_id = "{{ .ProjectId }}" + name = "cdn_acc_test_zone" + dns_name = "{{ .DNSName }}" + contact_email = "aa@bb.cc" + type = "primary" + default_ttl = 3600 } -func configDatasources(regions, cert, key string, geofencingCountries []string) string { +resource "stackit_dns_record_set" "dns_record" { + project_id = "{{ .ProjectId }}" + zone_id = stackit_dns_zone.dns_zone.zone_id + name = "{{ .CustomDomainPrefix }}" + type = "CNAME" + records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] +} +` + +const customDomainTmpl = ` +resource "stackit_cdn_custom_domain" "custom_domain" { + project_id = stackit_cdn_distribution.distribution.project_id + distribution_id = stackit_cdn_distribution.distribution.distribution_id + name = "${stackit_dns_record_set.dns_record.name}.${stackit_dns_zone.dns_zone.dns_name}" + certificate = { + certificate = {{ .Cert | printf "%q" }} + private_key = {{ .Key | printf "%q" }} + } +} +` + +// 3. Implement the Renderer +func renderConfig(conf distributionConfig, includeCustomDomain bool) string { + // Wrapper struct to include global provider config for the template + type templateData struct { + distributionConfig + ProviderConfig string + } + + data := templateData{ + distributionConfig: conf, + ProviderConfig: testutil.CdnProviderConfig(), + } + + // Helper to format go slices into HCL lists + funcMap := template.FuncMap{ + "stringList": func(s []string) string { + if len(s) == 0 { + return "[]" + } + var quoted []string + for _, item := range s { + quoted = append(quoted, fmt.Sprintf("%q", item)) + } + return fmt.Sprintf("[%s]", strings.Join(quoted, ", ")) + }, + } + + // Parse distribution template + tmpl, err := template.New("distribution").Funcs(funcMap).Parse(distributionTmpl) + if err != nil { + panic(fmt.Errorf("failed to parse distribution template: %w", err)) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + panic(fmt.Errorf("failed to render distribution template: %w", err)) + } + + // Parse custom domain template if needed + if includeCustomDomain { + cdTmpl, err := template.New("customDomain").Funcs(funcMap).Parse(customDomainTmpl) + if err != nil { + panic(fmt.Errorf("failed to parse custom domain template: %w", err)) + } + if err := cdTmpl.Execute(&buf, data); err != nil { + panic(fmt.Errorf("failed to render custom domain template: %w", err)) + } + } + + return buf.String() +} + +func configDatasources(conf distributionConfig) string { + baseConfig := renderConfig(conf, true) // Includes custom domain + return fmt.Sprintf(` %s data "stackit_cdn_distribution" "distribution" { - project_id = stackit_cdn_distribution.distribution.project_id + project_id = stackit_cdn_distribution.distribution.project_id distribution_id = stackit_cdn_distribution.distribution.distribution_id } data "stackit_cdn_custom_domain" "custom_domain" { - project_id = stackit_cdn_custom_domain.custom_domain.project_id - distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id - name = stackit_cdn_custom_domain.custom_domain.name - + project_id = stackit_cdn_custom_domain.custom_domain.project_id + distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id + name = stackit_cdn_custom_domain.custom_domain.name } - `, configCustomDomainResources(regions, cert, key, geofencingCountries)) + `, baseConfig) } func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048) @@ -155,20 +209,45 @@ func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { }) } func TestAccCDNDistributionResource(t *testing.T) { - fullDomainName := fmt.Sprintf("%s.%s", instanceResource["custom_domain_prefix"], instanceResource["dns_name"]) + // we use a different domain prefix each test run due to inconsistent upstream release of domains, which might impair consecutive test runs + customDomainPrefix := uuid.NewString() + dnsName := fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]) + geofencedOriginURL := "https://test-backend-2.cdn-dev.runs.onstackit.cloud" + fullDomainName := fmt.Sprintf("%s.%s", customDomainPrefix, dnsName) organization := fmt.Sprintf("organization-%s", uuid.NewString()) cert, key := makeCertAndKey(t, organization) - geofencing := []string{"DE", "ES"} + // Setup Base Configuration + baseConf := distributionConfig{ + ProjectId: testutil.ProjectId, + BackendType: "http", + OriginURL: "https://test-backend-1.cdn-dev.runs.onstackit.cloud", + DNSName: dnsName, + CustomDomainPrefix: customDomainPrefix, + OptimizerEnabled: true, + Regions: []string{"EU", "US"}, + BlockedCountries: []string{"CU", "AQ"}, // Do NOT use DE or AT here, because the request might be blocked by bunny at the time of creation - don't lock yourself out + Geofencing: map[string][]string{ + geofencedOriginURL: {"DE", "ES"}, + }, + } + + // Prepare updated config organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString()) cert_updated, key_updated := makeCertAndKey(t, organization_updated) + + updatedConf := baseConf + updatedConf.Regions = []string{"EU", "US", "ASIA"} + updatedConf.Cert = string(cert_updated) + updatedConf.Key = string(key_updated) + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckCDNDistributionDestroy, Steps: []resource.TestStep{ // Distribution Create { - Config: configResources(instanceResource["config_regions"], geofencing), + Config: renderConfig(baseConf, false), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), @@ -185,12 +264,12 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr( "stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]), + fmt.Sprintf("config.backend.geofencing.%s.0", geofencedOriginURL), "DE", ), resource.TestCheckResourceAttr( "stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]), + fmt.Sprintf("config.backend.geofencing.%s.1", geofencedOriginURL), "ES", ), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), @@ -200,7 +279,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Wait step, that confirms the CNAME record has "propagated" { - Config: configResources(instanceResource["config_regions"], geofencing), + Config: renderConfig(baseConf, false), Check: func(_ *terraform.State) error { _, err := blockUntilDomainResolves(fullDomainName) return err @@ -208,7 +287,12 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Custom Domain Create { - Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key), geofencing), + Config: func() string { + c := baseConf + c.Cert = string(cert) + c.Key = string(key) + return renderConfig(c, true) + }(), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), @@ -262,7 +346,12 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Data Source { - Config: configDatasources(instanceResource["config_regions"], string(cert), string(key), geofencing), + Config: func() string { + c := baseConf + c.Cert = string(cert) + c.Key = string(key) + return configDatasources(c) + }(), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "created_at"), @@ -277,12 +366,12 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr( "data.stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]), + fmt.Sprintf("config.backend.geofencing.%s.0", geofencedOriginURL), "DE", ), resource.TestCheckResourceAttr( "data.stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]), + fmt.Sprintf("config.backend.geofencing.%s.1", geofencedOriginURL), "ES", ), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.0", "EU"), @@ -301,7 +390,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Update { - Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated), geofencing), + Config: renderConfig(updatedConf, true), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), @@ -330,6 +419,24 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"), ), }, + // Bug Fix Verification: Omitted Field Handling + // + // This step verifies that omitting 'blocked_countries' from the Terraform configuration + // (by setting the pointer to nil) does not cause an "inconsistent result" error. + // + // Previously, omitting the field resulted in a 'null' config, but the API returned an + // empty list '[]', causing a state mismatch. The 'Default' modifier in the schema now + // ensures the missing config is treated as an empty list, matching the API response. + { + Config: func() string { + c := baseConf + c.BlockedCountries = nil // Empty list means it won't render + return renderConfig(c, false) + }(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "0"), + ), + }, }, }) } From 226f7b3d0c311922852b363c202792f8b7f719c4 Mon Sep 17 00:00:00 2001 From: Anton Pavlov <15091368+pavlov-tony@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:17:33 +0100 Subject: [PATCH 4/6] Address linters issues in cdn package --- stackit/internal/services/cdn/cdn_acc_test.go | 11 ++++++----- .../services/cdn/distribution/resource_test.go | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 63752cfad..ed48808e2 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -102,10 +102,10 @@ resource "stackit_cdn_custom_domain" "custom_domain" { ` // 3. Implement the Renderer -func renderConfig(conf distributionConfig, includeCustomDomain bool) string { +func renderConfig(conf *distributionConfig, includeCustomDomain bool) string { // Wrapper struct to include global provider config for the template type templateData struct { - distributionConfig + *distributionConfig ProviderConfig string } @@ -153,7 +153,7 @@ func renderConfig(conf distributionConfig, includeCustomDomain bool) string { return buf.String() } -func configDatasources(conf distributionConfig) string { +func configDatasources(conf *distributionConfig) string { baseConfig := renderConfig(conf, true) // Includes custom domain return fmt.Sprintf(` @@ -218,7 +218,7 @@ func TestAccCDNDistributionResource(t *testing.T) { cert, key := makeCertAndKey(t, organization) // Setup Base Configuration - baseConf := distributionConfig{ + baseConf := &distributionConfig{ ProjectId: testutil.ProjectId, BackendType: "http", OriginURL: "https://test-backend-1.cdn-dev.runs.onstackit.cloud", @@ -236,7 +236,8 @@ func TestAccCDNDistributionResource(t *testing.T) { organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString()) cert_updated, key_updated := makeCertAndKey(t, organization_updated) - updatedConf := baseConf + baseConfCopy := *baseConf + updatedConf := &baseConfCopy updatedConf.Regions = []string{"EU", "US", "ASIA"} updatedConf.Cert = string(cert_updated) updatedConf.Key = string(key_updated) diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index eec9ddc99..79e191a56 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -70,7 +70,7 @@ func TestToCreatePayload(t *testing.T) { "testHeader1": "testHeaderValue1", }, Geofencing: &map[string][]string{ - "https://de.mycoolapp.com": []string{"DE", "FR"}, + "https://de.mycoolapp.com": {"DE", "FR"}, }, Type: cdn.PtrString("http"), }, @@ -98,7 +98,7 @@ func TestToCreatePayload(t *testing.T) { "testHeader1": "testHeaderValue1", }, Geofencing: &map[string][]string{ - "https://de.mycoolapp.com": []string{"DE", "FR"}, + "https://de.mycoolapp.com": {"DE", "FR"}, }, Type: cdn.PtrString("http"), }, From d3cf78cee9957c345fc78dd73def1174afa2688c Mon Sep 17 00:00:00 2001 From: Anton Pavlov <15091368+pavlov-tony@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:04:44 +0100 Subject: [PATCH 5/6] Revert "Adjust acceptance tests for cdn service" This reverts commit 0fc5eef0bce4504b8f23e26bcc307e3793b9d7e4. --- stackit/internal/services/cdn/cdn_acc_test.go | 282 ++++++------------ 1 file changed, 87 insertions(+), 195 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index ed48808e2..0dd031a5b 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -1,7 +1,6 @@ package cdn_test import ( - "bytes" "context" cryptoRand "crypto/rand" "crypto/rsa" @@ -13,7 +12,6 @@ import ( "net" "strings" "testing" - "text/template" "time" "github.com/google/uuid" @@ -26,150 +24,98 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// 1. Define the Configuration Struct -type distributionConfig struct { - ProjectId string - BackendType string - OriginURL string - Geofencing map[string][]string - Regions []string - BlockedCountries []string - OptimizerEnabled bool - DNSName string - CustomDomainPrefix string - Cert string - Key string +var instanceResource = map[string]string{ + "project_id": testutil.ProjectId, + "config_backend_type": "http", + "config_backend_origin_url": "https://test-backend-1.cdn-dev.runs.onstackit.cloud", + "config_regions": "\"EU\", \"US\"", + "config_regions_updated": "\"EU\", \"US\", \"ASIA\"", + "blocked_countries": "\"CU\", \"AQ\"", // Do NOT use DE or AT here, because the request might be blocked by bunny at the time of creation - don't lock yourself out + "custom_domain_prefix": uuid.NewString(), // we use a different domain prefix each test run due to inconsistent upstream release of domains, which might impair consecutive test runs + "dns_name": fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]), } -// 2. Define the Template -const distributionTmpl = ` -{{ .ProviderConfig }} - -resource "stackit_cdn_distribution" "distribution" { - project_id = "{{ .ProjectId }}" - config = { - backend = { - type = "{{ .BackendType }}" - origin_url = "{{ .OriginURL }}" - {{- if .Geofencing }} - geofencing = { - {{- range $url, $countries := .Geofencing }} - "{{ $url }}" = {{ $countries | stringList }} - {{- end }} - } - {{- end }} - } - regions = {{ .Regions | stringList }} - - {{- if .BlockedCountries }} - blocked_countries = {{ .BlockedCountries | stringList }} - {{- end }} - - optimizer = { - enabled = {{ .OptimizerEnabled }} - } - } -} - -resource "stackit_dns_zone" "dns_zone" { - project_id = "{{ .ProjectId }}" - name = "cdn_acc_test_zone" - dns_name = "{{ .DNSName }}" - contact_email = "aa@bb.cc" - type = "primary" - default_ttl = 3600 -} - -resource "stackit_dns_record_set" "dns_record" { - project_id = "{{ .ProjectId }}" - zone_id = stackit_dns_zone.dns_zone.zone_id - name = "{{ .CustomDomainPrefix }}" - type = "CNAME" - records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] -} -` - -const customDomainTmpl = ` -resource "stackit_cdn_custom_domain" "custom_domain" { - project_id = stackit_cdn_distribution.distribution.project_id - distribution_id = stackit_cdn_distribution.distribution.distribution_id - name = "${stackit_dns_record_set.dns_record.name}.${stackit_dns_zone.dns_zone.dns_name}" - certificate = { - certificate = {{ .Cert | printf "%q" }} - private_key = {{ .Key | printf "%q" }} - } -} -` - -// 3. Implement the Renderer -func renderConfig(conf *distributionConfig, includeCustomDomain bool) string { - // Wrapper struct to include global provider config for the template - type templateData struct { - *distributionConfig - ProviderConfig string - } - - data := templateData{ - distributionConfig: conf, - ProviderConfig: testutil.CdnProviderConfig(), - } - - // Helper to format go slices into HCL lists - funcMap := template.FuncMap{ - "stringList": func(s []string) string { - if len(s) == 0 { - return "[]" - } - var quoted []string - for _, item := range s { - quoted = append(quoted, fmt.Sprintf("%q", item)) - } - return fmt.Sprintf("[%s]", strings.Join(quoted, ", ")) - }, - } - - // Parse distribution template - tmpl, err := template.New("distribution").Funcs(funcMap).Parse(distributionTmpl) - if err != nil { - panic(fmt.Errorf("failed to parse distribution template: %w", err)) +func configResources(regions string, geofencingCountries []string) string { + var quotedCountries []string + for _, country := range geofencingCountries { + quotedCountries = append(quotedCountries, fmt.Sprintf(`%q`, country)) } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - panic(fmt.Errorf("failed to render distribution template: %w", err)) - } - - // Parse custom domain template if needed - if includeCustomDomain { - cdTmpl, err := template.New("customDomain").Funcs(funcMap).Parse(customDomainTmpl) - if err != nil { - panic(fmt.Errorf("failed to parse custom domain template: %w", err)) - } - if err := cdTmpl.Execute(&buf, data); err != nil { - panic(fmt.Errorf("failed to render custom domain template: %w", err)) - } - } - - return buf.String() + geofencingList := strings.Join(quotedCountries, ",") + return fmt.Sprintf(` + %s + + resource "stackit_cdn_distribution" "distribution" { + project_id = "%s" + config = { + backend = { + type = "http" + origin_url = "%s" + geofencing = { + "%s" = [%s] + } + } + regions = [%s] + blocked_countries = [%s] + + optimizer = { + enabled = true + } + } + } + + resource "stackit_dns_zone" "dns_zone" { + project_id = "%s" + name = "cdn_acc_test_zone" + dns_name = "%s" + contact_email = "aa@bb.cc" + type = "primary" + default_ttl = 3600 + } + resource "stackit_dns_record_set" "dns_record" { + project_id = "%s" + zone_id = stackit_dns_zone.dns_zone.zone_id + name = "%s" + type = "CNAME" + records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] + } + `, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], instanceResource["config_backend_origin_url"], geofencingList, + regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"], + testutil.ProjectId, instanceResource["custom_domain_prefix"]) } -func configDatasources(conf *distributionConfig) string { - baseConfig := renderConfig(conf, true) // Includes custom domain +func configCustomDomainResources(regions, cert, key string, geofencingCountries []string) string { + return fmt.Sprintf(` + %s + + resource "stackit_cdn_custom_domain" "custom_domain" { + project_id = stackit_cdn_distribution.distribution.project_id + distribution_id = stackit_cdn_distribution.distribution.distribution_id + name = "${stackit_dns_record_set.dns_record.name}.${stackit_dns_zone.dns_zone.dns_name}" + certificate = { + certificate = %q + private_key = %q + } + } +`, configResources(regions, geofencingCountries), cert, key) +} +func configDatasources(regions, cert, key string, geofencingCountries []string) string { return fmt.Sprintf(` %s data "stackit_cdn_distribution" "distribution" { - project_id = stackit_cdn_distribution.distribution.project_id + project_id = stackit_cdn_distribution.distribution.project_id distribution_id = stackit_cdn_distribution.distribution.distribution_id } data "stackit_cdn_custom_domain" "custom_domain" { - project_id = stackit_cdn_custom_domain.custom_domain.project_id - distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id - name = stackit_cdn_custom_domain.custom_domain.name + project_id = stackit_cdn_custom_domain.custom_domain.project_id + distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id + name = stackit_cdn_custom_domain.custom_domain.name + } - `, baseConfig) + `, configCustomDomainResources(regions, cert, key, geofencingCountries)) } func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048) @@ -209,46 +155,20 @@ func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { }) } func TestAccCDNDistributionResource(t *testing.T) { - // we use a different domain prefix each test run due to inconsistent upstream release of domains, which might impair consecutive test runs - customDomainPrefix := uuid.NewString() - dnsName := fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]) - geofencedOriginURL := "https://test-backend-2.cdn-dev.runs.onstackit.cloud" - fullDomainName := fmt.Sprintf("%s.%s", customDomainPrefix, dnsName) + fullDomainName := fmt.Sprintf("%s.%s", instanceResource["custom_domain_prefix"], instanceResource["dns_name"]) organization := fmt.Sprintf("organization-%s", uuid.NewString()) cert, key := makeCertAndKey(t, organization) + geofencing := []string{"DE", "ES"} - // Setup Base Configuration - baseConf := &distributionConfig{ - ProjectId: testutil.ProjectId, - BackendType: "http", - OriginURL: "https://test-backend-1.cdn-dev.runs.onstackit.cloud", - DNSName: dnsName, - CustomDomainPrefix: customDomainPrefix, - OptimizerEnabled: true, - Regions: []string{"EU", "US"}, - BlockedCountries: []string{"CU", "AQ"}, // Do NOT use DE or AT here, because the request might be blocked by bunny at the time of creation - don't lock yourself out - Geofencing: map[string][]string{ - geofencedOriginURL: {"DE", "ES"}, - }, - } - - // Prepare updated config organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString()) cert_updated, key_updated := makeCertAndKey(t, organization_updated) - - baseConfCopy := *baseConf - updatedConf := &baseConfCopy - updatedConf.Regions = []string{"EU", "US", "ASIA"} - updatedConf.Cert = string(cert_updated) - updatedConf.Key = string(key_updated) - resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckCDNDistributionDestroy, Steps: []resource.TestStep{ // Distribution Create { - Config: renderConfig(baseConf, false), + Config: configResources(instanceResource["config_regions"], geofencing), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), @@ -265,12 +185,12 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr( "stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.0", geofencedOriginURL), + fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]), "DE", ), resource.TestCheckResourceAttr( "stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.1", geofencedOriginURL), + fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]), "ES", ), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), @@ -280,7 +200,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Wait step, that confirms the CNAME record has "propagated" { - Config: renderConfig(baseConf, false), + Config: configResources(instanceResource["config_regions"], geofencing), Check: func(_ *terraform.State) error { _, err := blockUntilDomainResolves(fullDomainName) return err @@ -288,12 +208,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Custom Domain Create { - Config: func() string { - c := baseConf - c.Cert = string(cert) - c.Key = string(key) - return renderConfig(c, true) - }(), + Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key), geofencing), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), @@ -347,12 +262,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Data Source { - Config: func() string { - c := baseConf - c.Cert = string(cert) - c.Key = string(key) - return configDatasources(c) - }(), + Config: configDatasources(instanceResource["config_regions"], string(cert), string(key), geofencing), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "created_at"), @@ -367,12 +277,12 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr( "data.stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.0", geofencedOriginURL), + fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]), "DE", ), resource.TestCheckResourceAttr( "data.stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.1", geofencedOriginURL), + fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]), "ES", ), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.0", "EU"), @@ -391,7 +301,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Update { - Config: renderConfig(updatedConf, true), + Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated), geofencing), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), @@ -420,24 +330,6 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"), ), }, - // Bug Fix Verification: Omitted Field Handling - // - // This step verifies that omitting 'blocked_countries' from the Terraform configuration - // (by setting the pointer to nil) does not cause an "inconsistent result" error. - // - // Previously, omitting the field resulted in a 'null' config, but the API returned an - // empty list '[]', causing a state mismatch. The 'Default' modifier in the schema now - // ensures the missing config is treated as an empty list, matching the API response. - { - Config: func() string { - c := baseConf - c.BlockedCountries = nil // Empty list means it won't render - return renderConfig(c, false) - }(), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "0"), - ), - }, }, }) } From 217976dedcee416ac3ee88f3bb2ced673b6811ca Mon Sep 17 00:00:00 2001 From: Anton Pavlov <15091368+pavlov-tony@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:07:41 +0100 Subject: [PATCH 6/6] Add acc test case covering fixed bug in cdn --- stackit/internal/services/cdn/cdn_acc_test.go | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 0dd031a5b..995231e49 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -35,13 +35,19 @@ var instanceResource = map[string]string{ "dns_name": fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]), } -func configResources(regions string, geofencingCountries []string) string { +func configResources(regions string, geofencingCountries []string, blockedCountries *string) string { var quotedCountries []string for _, country := range geofencingCountries { quotedCountries = append(quotedCountries, fmt.Sprintf(`%q`, country)) } geofencingList := strings.Join(quotedCountries, ",") + + blockedCountriesConfig := "" + if blockedCountries != nil { + blockedCountriesConfig = fmt.Sprintf("blocked_countries = [%s]", *blockedCountries) + } + return fmt.Sprintf(` %s @@ -56,7 +62,7 @@ func configResources(regions string, geofencingCountries []string) string { } } regions = [%s] - blocked_countries = [%s] + %s optimizer = { enabled = true @@ -80,11 +86,11 @@ func configResources(regions string, geofencingCountries []string) string { records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] } `, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], instanceResource["config_backend_origin_url"], geofencingList, - regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"], + regions, blockedCountriesConfig, testutil.ProjectId, instanceResource["dns_name"], testutil.ProjectId, instanceResource["custom_domain_prefix"]) } -func configCustomDomainResources(regions, cert, key string, geofencingCountries []string) string { +func configCustomDomainResources(regions, cert, key string, geofencingCountries []string, blockedCountries *string) string { return fmt.Sprintf(` %s @@ -97,10 +103,10 @@ func configCustomDomainResources(regions, cert, key string, geofencingCountries private_key = %q } } -`, configResources(regions, geofencingCountries), cert, key) +`, configResources(regions, geofencingCountries, blockedCountries), cert, key) } -func configDatasources(regions, cert, key string, geofencingCountries []string) string { +func configDatasources(regions, cert, key string, geofencingCountries []string, blockedCountries *string) string { return fmt.Sprintf(` %s @@ -115,7 +121,7 @@ func configDatasources(regions, cert, key string, geofencingCountries []string) name = stackit_cdn_custom_domain.custom_domain.name } - `, configCustomDomainResources(regions, cert, key, geofencingCountries)) + `, configCustomDomainResources(regions, cert, key, geofencingCountries, blockedCountries)) } func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048) @@ -162,13 +168,17 @@ func TestAccCDNDistributionResource(t *testing.T) { organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString()) cert_updated, key_updated := makeCertAndKey(t, organization_updated) + + // Helper for default blocked countries + defaultBlockedCountries := cdn.PtrString(instanceResource["blocked_countries"]) + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckCDNDistributionDestroy, Steps: []resource.TestStep{ // Distribution Create { - Config: configResources(instanceResource["config_regions"], geofencing), + Config: configResources(instanceResource["config_regions"], geofencing, defaultBlockedCountries), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), @@ -200,7 +210,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Wait step, that confirms the CNAME record has "propagated" { - Config: configResources(instanceResource["config_regions"], geofencing), + Config: configResources(instanceResource["config_regions"], geofencing, defaultBlockedCountries), Check: func(_ *terraform.State) error { _, err := blockUntilDomainResolves(fullDomainName) return err @@ -208,7 +218,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Custom Domain Create { - Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key), geofencing), + Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key), geofencing, defaultBlockedCountries), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), @@ -262,7 +272,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Data Source { - Config: configDatasources(instanceResource["config_regions"], string(cert), string(key), geofencing), + Config: configDatasources(instanceResource["config_regions"], string(cert), string(key), geofencing, defaultBlockedCountries), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "created_at"), @@ -301,7 +311,7 @@ func TestAccCDNDistributionResource(t *testing.T) { }, // Update { - Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated), geofencing), + Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated), geofencing, defaultBlockedCountries), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), @@ -330,6 +340,20 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"), ), }, + // Bug Fix Verification: Omitted Field Handling + // + // This step verifies that omitting 'blocked_countries' from the Terraform configuration + // (by setting the pointer to nil) does not cause an "inconsistent result" error. + // + // Previously, omitting the field resulted in a 'null' config, but the API returned an + // empty list '[]', causing a state mismatch. The 'Default' modifier in the schema now + // ensures the missing config is treated as an empty list, matching the API response. + { + Config: configResources(instanceResource["config_regions"], geofencing, nil), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "0"), + ), + }, }, }) }