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"), + ), + }, }, }) } diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index cbd215c82..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{})), }, }, }, @@ -812,6 +823,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..79e191a56 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": {"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": {"DE", "FR"}, + }, + Type: cdn.PtrString("http"), + }, + }, Regions: &[]cdn.Region{"EU", "US"}, Optimizer: cdn.NewOptimizer(true), BlockedCountries: &[]string{"XX", "YY", "ZZ"},