diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index a4f28cc7..84791109 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -43,6 +43,10 @@ data "stackit_cdn_distribution" "example" { ### Nested Schema for `config` +Optional: + +- `blocked_countries` (List of String) The configured countries where distribution of content is blocked + Read-Only: - `backend` (Attributes) The configured backend for the distribution (see [below for nested schema](#nestedatt--config--backend)) diff --git a/docs/guides/stackit_cdn_with_custom_domain.md b/docs/guides/stackit_cdn_with_custom_domain.md index aa915136..0c90a88d 100644 --- a/docs/guides/stackit_cdn_with_custom_domain.md +++ b/docs/guides/stackit_cdn_with_custom_domain.md @@ -21,7 +21,8 @@ This guide outlines the process of creating a STACKIT CDN distribution and confi type = "http" origin_url = "mybackend.onstackit.cloud" } - regions = ["EU", "US", "ASIA", "AF", "SA"] + regions = ["EU", "US", "ASIA", "AF", "SA"] + blocked_countries = ["DE", "AT", "CH"] } } diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index 5a52fd20..c9ad2688 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -23,7 +23,9 @@ resource "stackit_cdn_distribution" "example_distribution" { type = "http" origin_url = "mybackend.onstackit.cloud" } - regions = ["EU", "US", "ASIA", "AF", "SA"] + regions = ["EU", "US", "ASIA", "AF", "SA"] + blocked_countries = ["DE", "AT", "CH"] + optimizer = { enabled = true } @@ -59,6 +61,7 @@ Required: Optional: +- `blocked_countries` (List of String) The configured countries where distribution of content is blocked - `optimizer` (Attributes) Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience. (see [below for nested schema](#nestedatt--config--optimizer)) diff --git a/examples/resources/stackit_cdn_distribution/resource.tf b/examples/resources/stackit_cdn_distribution/resource.tf index 39565b22..ebdae25e 100644 --- a/examples/resources/stackit_cdn_distribution/resource.tf +++ b/examples/resources/stackit_cdn_distribution/resource.tf @@ -5,7 +5,9 @@ resource "stackit_cdn_distribution" "example_distribution" { type = "http" origin_url = "mybackend.onstackit.cloud" } - regions = ["EU", "US", "ASIA", "AF", "SA"] + regions = ["EU", "US", "ASIA", "AF", "SA"] + blocked_countries = ["DE", "AT", "CH"] + optimizer = { enabled = true } diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 43d24520..b723f5d8 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -24,6 +24,7 @@ var instanceResource = map[string]string{ "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 } @@ -38,7 +39,9 @@ func configResources(regions string) string { type = "http" origin_url = "%s" } - regions = [%s] + regions = [%s] + blocked_countries = [%s] + optimizer = { enabled = true } @@ -60,7 +63,9 @@ func configResources(regions string) string { type = "CNAME" records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] } - `, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], regions, testutil.ProjectId, testutil.ProjectId, instanceResource["custom_domain_prefix"]) + `, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], + regions, instanceResource["blocked_countries"], testutil.ProjectId, + testutil.ProjectId, instanceResource["custom_domain_prefix"]) } func configCustomDomainResources(regions string) string { @@ -111,6 +116,9 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), @@ -191,6 +199,9 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.1", "US"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"), @@ -217,6 +228,9 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.2", "ASIA"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index 09afc4ff..590af144 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -149,6 +149,11 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe Description: schemaDescriptions["config_regions"], ElementType: types.StringType, }, + "blocked_countries": schema.ListAttribute{ + Optional: true, + Description: schemaDescriptions["config_blocked_countries"], + ElementType: types.StringType, + }, "optimizer": schema.SingleNestedAttribute{ Description: schemaDescriptions["config_optimizer"], Computed: true, diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index d1cc4e33..8c4125f7 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -57,6 +57,7 @@ var schemaDescriptions = map[string]string{ "config_optimizer": "Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience.", "config_backend_origin_url": "The configured backend type for the distribution", "config_backend_origin_request_headers": "The configured origin request headers for the backend", + "config_blocked_countries": "The configured countries where distribution of content is blocked", "domain_name": "The name of the domain", "domain_status": "The status of the domain", "domain_type": "The type of the domain. Each distribution has one domain of type \"managed\", and domains of type \"custom\" may be additionally created by the user", @@ -76,13 +77,16 @@ type Model struct { } type distributionConfig struct { - Backend backend `tfsdk:"backend"` // The backend associated with the distribution - Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached - Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration + Backend backend `tfsdk:"backend"` // The backend associated with the distribution + Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached + BlockedCountries *[]string `tfsdk:"blocked_countries"` // The countries for which content will be blocked + Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration } + type optimizerConfig struct { Enabled types.Bool `tfsdk:"enabled"` } + type backend struct { Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" backend is supported OriginURL string `tfsdk:"origin_url"` // The origin URL of the backend @@ -90,8 +94,9 @@ type backend struct { } var configTypes = map[string]attr.Type{ - "backend": types.ObjectType{AttrTypes: backendTypes}, - "regions": types.ListType{ElemType: types.StringType}, + "backend": types.ObjectType{AttrTypes: backendTypes}, + "regions": types.ListType{ElemType: types.StringType}, + "blocked_countries": types.ListType{ElemType: types.StringType}, "optimizer": types.ObjectType{ AttrTypes: optimizerTypes, }, @@ -258,6 +263,11 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Description: schemaDescriptions["config_regions"], ElementType: types.StringType, }, + "blocked_countries": schema.ListAttribute{ + Optional: true, + Description: schemaDescriptions["config_blocked_countries"], + ElementType: types.StringType, + }, }, }, }, @@ -378,6 +388,26 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe regions = append(regions, *regionEnum) } + // blockedCountries + // Use a pointer to a slice to distinguish between an empty list (unblock all) and nil (no change). + var blockedCountries *[]string + if configModel.BlockedCountries != nil { + // Use a temporary slice + tempBlockedCountries := []string{} + + for _, blockedCountry := range *configModel.BlockedCountries { + validatedBlockedCountry, err := validateCountryCode(blockedCountry) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Blocked countries: %v", err)) + return + } + tempBlockedCountries = append(tempBlockedCountries, validatedBlockedCountry) + } + + // Point to the populated slice + blockedCountries = &tempBlockedCountries + } + configPatch := &cdn.ConfigPatch{ Backend: &cdn.ConfigPatchBackend{ HttpBackendPatch: &cdn.HttpBackendPatch{ @@ -386,7 +416,8 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Type: &configModel.Backend.Type, }, }, - Regions: ®ions, + Regions: ®ions, + BlockedCountries: blockedCountries, } if !utils.IsUndefined(configModel.Optimizer) { @@ -411,7 +442,9 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe }).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Patch distribution: %v", err)) + return } + waitResp, err := wait.UpdateDistributionWaitHandler(ctx, r.client, projectId, distributionId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Waiting for update: %v", err)) @@ -423,6 +456,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -501,6 +535,7 @@ func mapFields(distribution *cdn.Distribution, model *Model) error { model.CreatedAt = types.StringValue(distribution.CreatedAt.String()) model.UpdatedAt = types.StringValue(distribution.UpdatedAt.String()) + // distributionErrors distributionErrors := []attr.Value{} if distribution.Errors != nil { for _, e := range *distribution.Errors { @@ -513,6 +548,7 @@ func mapFields(distribution *cdn.Distribution, model *Model) error { } model.Errors = modelErrors + // regions regions := []attr.Value{} for _, r := range *distribution.Config.Regions { regions = append(regions, types.StringValue(string(r))) @@ -521,6 +557,21 @@ func mapFields(distribution *cdn.Distribution, model *Model) error { if diags.HasError() { return core.DiagsToError(diags) } + + // blockedCountries + var blockedCountries []attr.Value + if distribution.Config != nil && distribution.Config.BlockedCountries != nil { + for _, c := range *distribution.Config.BlockedCountries { + blockedCountries = append(blockedCountries, types.StringValue(string(c))) + } + } + + modelBlockedCountries, diags := types.ListValue(types.StringType, blockedCountries) + if diags.HasError() { + return core.DiagsToError(diags) + } + + // originRequestHeaders originRequestHeaders := types.MapNull(types.StringType) if origHeaders := distribution.Config.Backend.HttpBackend.OriginRequestHeaders; origHeaders != nil && len(*origHeaders) > 0 { headers := map[string]attr.Value{} @@ -557,9 +608,10 @@ func mapFields(distribution *cdn.Distribution, model *Model) error { } } cfg, diags := types.ObjectValue(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": modelRegions, - "optimizer": optimizerVal, + "backend": backend, + "regions": modelRegions, + "blocked_countries": modelBlockedCountries, + "optimizer": optimizerVal, }) if diags.HasError() { return core.DiagsToError(diags) @@ -624,6 +676,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution IntentId: cdn.PtrString(uuid.NewString()), OriginUrl: cfg.Backend.HttpBackend.OriginUrl, Regions: cfg.Regions, + BlockedCountries: cfg.BlockedCountries, OriginRequestHeaders: cfg.Backend.HttpBackend.OriginRequestHeaders, Optimizer: optimizer, } @@ -646,6 +699,8 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { if diags.HasError() { return nil, core.DiagsToError(diags) } + + // regions regions := []cdn.Region{} for _, r := range *configModel.Regions { regionEnum, err := cdn.NewRegionFromValue(r) @@ -655,6 +710,19 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { regions = append(regions, *regionEnum) } + // blockedCountries + var blockedCountries []string + if configModel.BlockedCountries != nil { + for _, blockedCountry := range *configModel.BlockedCountries { + validatedBlockedCountry, err := validateCountryCode(blockedCountry) + if err != nil { + return nil, err + } + blockedCountries = append(blockedCountries, validatedBlockedCountry) + } + } + + // originRequestHeaders originRequestHeaders := map[string]string{} if configModel.Backend.OriginRequestHeaders != nil { for k, v := range *configModel.Backend.OriginRequestHeaders { @@ -670,7 +738,8 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { Type: &configModel.Backend.Type, }, }, - Regions: ®ions, + Regions: ®ions, + BlockedCountries: &blockedCountries, } if !utils.IsUndefined(configModel.Optimizer) { @@ -687,3 +756,25 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { return cdnConfig, nil } + +// validateCountryCode checks for a valid country user input. This is just a quick check +// since the API already does a more thorough check. +func validateCountryCode(country string) (string, error) { + if len(country) != 2 { + return "", errors.New("country code must be exactly 2 characters long") + } + + upperCountry := strings.ToUpper(country) + + // Check if both characters are alphabetical letters within the ASCII range A-Z. + // Yes, we could use the unicode package, but we are only targeting ASCII letters specifically, so + // let's omit this dependency. + char1 := upperCountry[0] + char2 := upperCountry[1] + + if !((char1 >= 'A' && char1 <= 'Z') && (char2 >= 'A' && char2 <= 'Z')) { + return "", fmt.Errorf("country code '%s' must consist of two alphabetical letters (A-Z or a-z)", country) + } + + return upperCountry, nil +} diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index 970c7535..13e55c7a 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -24,13 +24,16 @@ func TestToCreatePayload(t *testing.T) { }) regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} regionsFixture := types.ListValueMust(types.StringType, regions) + blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")} + blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(true), }) config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": types.ObjectNull(optimizerTypes), + "backend": backend, + "regions": regionsFixture, + "blocked_countries": blockedCountriesFixture, + "optimizer": types.ObjectNull(optimizerTypes), }) modelFixture := func(mods ...func(*Model)) *Model { model := &Model{ @@ -55,17 +58,19 @@ func TestToCreatePayload(t *testing.T) { "testHeader0": "testHeaderValue0", "testHeader1": "testHeaderValue1", }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Regions: &[]cdn.Region{"EU", "US"}, + OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), + Regions: &[]cdn.Region{"EU", "US"}, + BlockedCountries: &[]string{"XX", "YY", "ZZ"}, }, IsValid: true, }, "happy_path_with_optimizer": { Input: modelFixture(func(m *Model) { m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": optimizer, + "backend": backend, + "regions": regionsFixture, + "optimizer": optimizer, + "blocked_countries": blockedCountriesFixture, }) }), Expected: &cdn.CreateDistributionPayload{ @@ -73,9 +78,10 @@ func TestToCreatePayload(t *testing.T) { "testHeader0": "testHeaderValue0", "testHeader1": "testHeaderValue1", }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Regions: &[]cdn.Region{"EU", "US"}, - Optimizer: cdn.NewOptimizer(true), + OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), + Regions: &[]cdn.Region{"EU", "US"}, + Optimizer: cdn.NewOptimizer(true), + BlockedCountries: &[]string{"XX", "YY", "ZZ"}, }, IsValid: true, }, @@ -127,11 +133,14 @@ func TestConvertConfig(t *testing.T) { }) regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} regionsFixture := types.ListValueMust(types.StringType, regions) + blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")} + blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{"enabled": types.BoolValue(true)}) config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": types.ObjectNull(optimizerTypes), + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), + "blocked_countries": blockedCountriesFixture, }) modelFixture := func(mods ...func(*Model)) *Model { model := &Model{ @@ -162,16 +171,18 @@ func TestConvertConfig(t *testing.T) { Type: cdn.PtrString("http"), }, }, - Regions: &[]cdn.Region{"EU", "US"}, + Regions: &[]cdn.Region{"EU", "US"}, + BlockedCountries: &[]string{"XX", "YY", "ZZ"}, }, IsValid: true, }, "happy_path_with_optimizer": { Input: modelFixture(func(m *Model) { m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": optimizer, + "backend": backend, + "regions": regionsFixture, + "optimizer": optimizer, + "blocked_countries": blockedCountriesFixture, }) }), Expected: &cdn.Config{ @@ -185,8 +196,9 @@ func TestConvertConfig(t *testing.T) { Type: cdn.PtrString("http"), }, }, - Regions: &[]cdn.Region{"EU", "US"}, - Optimizer: cdn.NewOptimizer(true), + Regions: &[]cdn.Region{"EU", "US"}, + Optimizer: cdn.NewOptimizer(true), + BlockedCountries: &[]string{"XX", "YY", "ZZ"}, }, IsValid: true, }, @@ -237,13 +249,16 @@ func TestMapFields(t *testing.T) { }) regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} regionsFixture := types.ListValueMust(types.StringType, regions) + blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")} + blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(true), }) config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": types.ObjectNull(optimizerTypes), + "backend": backend, + "regions": regionsFixture, + "blocked_countries": blockedCountriesFixture, + "optimizer": types.ObjectNull(optimizerTypes), }) emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{}) @@ -284,8 +299,9 @@ func TestMapFields(t *testing.T) { Type: cdn.PtrString("http"), }, }, - Regions: &[]cdn.Region{"EU", "US"}, - Optimizer: nil, + Regions: &[]cdn.Region{"EU", "US"}, + BlockedCountries: &[]string{"XX", "YY", "ZZ"}, + Optimizer: nil, }, CreatedAt: &createdAt, Domains: &[]cdn.Domain{ @@ -318,9 +334,10 @@ func TestMapFields(t *testing.T) { "happy_path_with_optimizer": { Expected: expectedModel(func(m *Model) { m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": optimizer, + "backend": backend, + "regions": regionsFixture, + "optimizer": optimizer, + "blocked_countries": blockedCountriesFixture, }) }), Input: distributionFixture(func(d *cdn.Distribution) { @@ -411,3 +428,108 @@ func TestMapFields(t *testing.T) { }) } } + +// TestValidateCountryCode tests the validateCountryCode function with a variety of inputs. +func TestValidateCountryCode(t *testing.T) { + testCases := []struct { + name string + inputCountry string + wantOutput string + expectError bool + expectedError string + }{ + // Happy Path + { + name: "Valid lowercase", + inputCountry: "us", + wantOutput: "US", + expectError: false, + }, + { + name: "Valid uppercase", + inputCountry: "DE", + wantOutput: "DE", + expectError: false, + }, + { + name: "Valid mixed case", + inputCountry: "cA", + wantOutput: "CA", + expectError: false, + }, + { + name: "Valid country code FR", + inputCountry: "fr", + wantOutput: "FR", + expectError: false, + }, + + // Error Scenarios + { + name: "Invalid length - too short", + inputCountry: "a", + wantOutput: "", + expectError: true, + expectedError: "country code must be exactly 2 characters long", + }, + { + name: "Invalid length - too long", + inputCountry: "USA", + wantOutput: "", + expectError: true, + expectedError: "country code must be exactly 2 characters long", + }, + { + name: "Invalid characters - contains number", + inputCountry: "U1", + wantOutput: "", + expectError: true, + expectedError: "country code 'U1' must consist of two alphabetical letters (A-Z or a-z)", + }, + { + name: "Invalid characters - contains symbol", + inputCountry: "D!", + wantOutput: "", + expectError: true, + expectedError: "country code 'D!' must consist of two alphabetical letters (A-Z or a-z)", + }, + { + name: "Invalid characters - both are numbers", + inputCountry: "42", + wantOutput: "", + expectError: true, + expectedError: "country code '42' must consist of two alphabetical letters (A-Z or a-z)", + }, + { + name: "Empty string", + inputCountry: "", + wantOutput: "", + expectError: true, + expectedError: "country code must be exactly 2 characters long", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gotOutput, err := validateCountryCode(tc.inputCountry) + + if tc.expectError { + if err == nil { + t.Errorf("expected an error for input '%s', but got none", tc.inputCountry) + } else if err.Error() != tc.expectedError { + t.Errorf("for input '%s', expected error '%s', but got '%s'", tc.inputCountry, tc.expectedError, err.Error()) + } + if gotOutput != "" { + t.Errorf("expected empty string on error, but got '%s'", gotOutput) + } + } else { + if err != nil { + t.Errorf("did not expect an error for input '%s', but got: %v", tc.inputCountry, err) + } + if gotOutput != tc.wantOutput { + t.Errorf("for input '%s', expected output '%s', but got '%s'", tc.inputCountry, tc.wantOutput, gotOutput) + } + } + }) + } +} diff --git a/templates/guides/stackit_cdn_with_custom_domain.md.tmpl b/templates/guides/stackit_cdn_with_custom_domain.md.tmpl index aa915136..0c90a88d 100644 --- a/templates/guides/stackit_cdn_with_custom_domain.md.tmpl +++ b/templates/guides/stackit_cdn_with_custom_domain.md.tmpl @@ -21,7 +21,8 @@ This guide outlines the process of creating a STACKIT CDN distribution and confi type = "http" origin_url = "mybackend.onstackit.cloud" } - regions = ["EU", "US", "ASIA", "AF", "SA"] + regions = ["EU", "US", "ASIA", "AF", "SA"] + blocked_countries = ["DE", "AT", "CH"] } }