Skip to content
48 changes: 36 additions & 12 deletions stackit/internal/services/cdn/cdn_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -56,7 +62,7 @@ func configResources(regions string, geofencingCountries []string) string {
}
}
regions = [%s]
blocked_countries = [%s]
%s

optimizer = {
enabled = true
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -200,15 +210,15 @@ 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
},
},
// 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),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
),
},
},
})
}
Expand Down
19 changes: 19 additions & 0 deletions stackit/internal/services/cdn/distribution/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{})),
},
},
},
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions stackit/internal/services/cdn/distribution/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
},
Expand All @@ -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"},
Expand Down