cdn add geofence feature (#1020)

* add geofencing attribute to "stackit_cdn_distribution"
This commit is contained in:
Politano 2025-10-15 10:56:47 +02:00 committed by GitHub
parent 87bc7415fc
commit f0433984f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 324 additions and 23 deletions

View file

@ -58,6 +58,7 @@ Read-Only:
Read-Only:
- `geofencing` (Map of List of String) A map of URLs to a list of countries where content is allowed.
- `origin_request_headers` (Map of String) The configured origin request headers for the backend
- `origin_url` (String) The configured backend type for the distribution
- `type` (String) The configured backend type. Supported values are: `http`.

View file

@ -22,6 +22,9 @@ resource "stackit_cdn_distribution" "example_distribution" {
backend = {
type = "http"
origin_url = "https://mybackend.onstackit.cloud"
geofencing = {
"https://mybackend.onstackit.cloud" = ["DE"]
}
}
regions = ["EU", "US", "ASIA", "AF", "SA"]
blocked_countries = ["DE", "AT", "CH"]
@ -80,6 +83,7 @@ Required:
Optional:
- `geofencing` (Map of List of String) A map of URLs to a list of countries where content is allowed.
- `origin_request_headers` (Map of String) The configured origin request headers for the backend

View file

@ -4,6 +4,9 @@ resource "stackit_cdn_distribution" "example_distribution" {
backend = {
type = "http"
origin_url = "https://mybackend.onstackit.cloud"
geofencing = {
"https://mybackend.onstackit.cloud" = ["DE"]
}
}
regions = ["EU", "US", "ASIA", "AF", "SA"]
blocked_countries = ["DE", "AT", "CH"]

View file

@ -35,7 +35,13 @@ var instanceResource = map[string]string{
"dns_name": fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]),
}
func configResources(regions string) string {
func configResources(regions string, geofencingCountries []string) string {
var quotedCountries []string
for _, country := range geofencingCountries {
quotedCountries = append(quotedCountries, fmt.Sprintf(`%q`, country))
}
geofencingList := strings.Join(quotedCountries, ",")
return fmt.Sprintf(`
%s
@ -45,6 +51,9 @@ func configResources(regions string) string {
backend = {
type = "http"
origin_url = "%s"
geofencing = {
"%s" = [%s]
}
}
regions = [%s]
blocked_countries = [%s]
@ -70,12 +79,12 @@ func configResources(regions string) string {
type = "CNAME"
records = ["${stackit_cdn_distribution.distribution.domains[0].name}."]
}
`, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"],
`, 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 configCustomDomainResources(regions, cert, key string) string {
func configCustomDomainResources(regions, cert, key string, geofencingCountries []string) string {
return fmt.Sprintf(`
%s
@ -88,10 +97,10 @@ func configCustomDomainResources(regions, cert, key string) string {
private_key = %q
}
}
`, configResources(regions), cert, key)
`, configResources(regions, geofencingCountries), cert, key)
}
func configDatasources(regions, cert, key string) string {
func configDatasources(regions, cert, key string, geofencingCountries []string) string {
return fmt.Sprintf(`
%s
@ -106,7 +115,7 @@ func configDatasources(regions, cert, key string) string {
name = stackit_cdn_custom_domain.custom_domain.name
}
`, configCustomDomainResources(regions, cert, key))
`, configCustomDomainResources(regions, cert, key, geofencingCountries))
}
func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) {
privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048)
@ -149,6 +158,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
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"}
organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString())
cert_updated, key_updated := makeCertAndKey(t, organization_updated)
@ -158,7 +168,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
Steps: []resource.TestStep{
// Distribution Create
{
Config: configResources(instanceResource["config_regions"]),
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"),
@ -173,6 +183,16 @@ func TestAccCDNDistributionResource(t *testing.T) {
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",
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", instanceResource["config_backend_origin_url"]),
"ES",
),
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"),
@ -180,7 +200,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
},
// Wait step, that confirms the CNAME record has "propagated"
{
Config: configResources(instanceResource["config_regions"]),
Config: configResources(instanceResource["config_regions"], geofencing),
Check: func(_ *terraform.State) error {
_, err := blockUntilDomainResolves(fullDomainName)
return err
@ -188,7 +208,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
},
// Custom Domain Create
{
Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key)),
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),
@ -242,7 +262,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
},
// Data Source
{
Config: configDatasources(instanceResource["config_regions"], string(cert), string(key)),
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"),
@ -255,6 +275,16 @@ func TestAccCDNDistributionResource(t *testing.T) {
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.type", "custom"),
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"]),
"DE",
),
resource.TestCheckResourceAttr(
"data.stackit_cdn_distribution.distribution",
fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]),
"ES",
),
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"),
@ -271,7 +301,7 @@ func TestAccCDNDistributionResource(t *testing.T) {
},
// Update
{
Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated)),
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"),

View file

@ -142,6 +142,13 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe
Description: schemaDescriptions["config_backend_origin_request_headers"],
ElementType: types.StringType,
},
"geofencing": schema.MapAttribute{
Description: "A map of URLs to a list of countries where content is allowed.",
Computed: true,
ElementType: types.ListType{
ElemType: types.StringType,
},
},
},
},
"regions": schema.ListAttribute{
@ -192,7 +199,7 @@ func (r *distributionDataSource) Read(ctx context.Context, req datasource.ReadRe
resp.State.RemoveResource(ctx)
return
}
err = mapFields(distributionResp.Distribution, &model)
err = mapFields(ctx, distributionResp.Distribution, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Error processing API response: %v", err))
return

View file

@ -8,10 +8,8 @@ import (
"strings"
"time"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
@ -28,8 +26,10 @@ import (
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
"github.com/stackitcloud/stackit-sdk-go/services/cdn/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
@ -88,9 +88,10 @@ type optimizerConfig struct {
}
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
OriginRequestHeaders *map[string]string `tfsdk:"origin_request_headers"` // Request headers that should be added by the CDN distribution to incoming requests
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
OriginRequestHeaders *map[string]string `tfsdk:"origin_request_headers"` // Request headers that should be added by the CDN distribution to incoming requests
Geofencing *map[string][]*string `tfsdk:"geofencing"` // The geofencing is an object mapping multiple alternative origins to country codes.
}
var configTypes = map[string]attr.Type{
@ -106,10 +107,15 @@ var optimizerTypes = map[string]attr.Type{
"enabled": types.BoolType,
}
var geofencingTypes = types.MapType{ElemType: types.ListType{
ElemType: types.StringType,
}}
var backendTypes = map[string]attr.Type{
"type": types.StringType,
"origin_url": types.StringType,
"origin_request_headers": types.MapType{ElemType: types.StringType},
"geofencing": geofencingTypes,
}
var domainTypes = map[string]attr.Type{
@ -256,6 +262,16 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques
Description: schemaDescriptions["config_backend_origin_request_headers"],
ElementType: types.StringType,
},
"geofencing": schema.MapAttribute{
Description: "A map of URLs to a list of countries where content is allowed.",
Optional: true,
ElementType: types.ListType{
ElemType: types.StringType,
},
Validators: []validator.Map{
mapvalidator.SizeAtLeast(1),
},
},
},
},
"regions": schema.ListAttribute{
@ -274,6 +290,43 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques
}
}
func (r *distributionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var model Model
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
if !utils.IsUndefined(model.Config) {
var config distributionConfig
if !model.Config.IsNull() {
diags := model.Config.As(ctx, &config, basetypes.ObjectAsOptions{})
if diags.HasError() {
return
}
if geofencing := config.Backend.Geofencing; geofencing != nil {
for url, region := range *geofencing {
if region == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be null.", url))
continue
}
if len(region) == 0 {
core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be empty.", url))
continue
}
for i, countryPtr := range region {
if countryPtr == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("Found a null value in the country list for URL %q at index %d.", url, i))
break
}
}
}
}
}
}
}
func (r *distributionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Plan.Get(ctx, &model)
@ -301,7 +354,7 @@ func (r *distributionResource) Create(ctx context.Context, req resource.CreateRe
return
}
err = mapFields(waitResp.Distribution, &model)
err = mapFields(ctx, waitResp.Distribution, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Processing API payload: %v", err))
return
@ -341,7 +394,7 @@ func (r *distributionResource) Read(ctx context.Context, req resource.ReadReques
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(cdnResp.Distribution, &model)
err = mapFields(ctx, cdnResp.Distribution, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN ditribution", fmt.Sprintf("Processing API payload: %v", err))
return
@ -408,12 +461,30 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe
blockedCountries = &tempBlockedCountries
}
geofencingPatch := map[string][]string{}
if configModel.Backend.Geofencing != nil {
gf := make(map[string][]string)
for url, countries := range *configModel.Backend.Geofencing {
countryStrings := make([]string, len(countries))
for i, countryPtr := range countries {
if countryPtr == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Geofencing url %q has a null value", url))
return
}
countryStrings[i] = *countryPtr
}
gf[url] = countryStrings
}
geofencingPatch = gf
}
configPatch := &cdn.ConfigPatch{
Backend: &cdn.ConfigPatchBackend{
HttpBackendPatch: &cdn.HttpBackendPatch{
OriginRequestHeaders: configModel.Backend.OriginRequestHeaders,
OriginUrl: &configModel.Backend.OriginURL,
Type: &configModel.Backend.Type,
Geofencing: &geofencingPatch, // Use the converted variable
},
},
Regions: &regions,
@ -451,7 +522,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe
return
}
err = mapFields(waitResp.Distribution, &model)
err = mapFields(ctx, waitResp.Distribution, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Processing API payload: %v", err))
return
@ -500,7 +571,7 @@ func (r *distributionResource) ImportState(ctx context.Context, req resource.Imp
tflog.Info(ctx, "CDN distribution state imported")
}
func mapFields(distribution *cdn.Distribution, model *Model) error {
func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model) error {
if distribution == nil {
return fmt.Errorf("response input is nil")
}
@ -584,11 +655,58 @@ func mapFields(distribution *cdn.Distribution, model *Model) error {
return core.DiagsToError(diags)
}
}
// geofencing
var oldConfig distributionConfig
oldGeofencingMap := make(map[string][]*string)
if !model.Config.IsNull() {
diags = model.Config.As(ctx, &oldConfig, basetypes.ObjectAsOptions{})
if diags.HasError() {
return core.DiagsToError(diags)
}
if oldConfig.Backend.Geofencing != nil {
oldGeofencingMap = *oldConfig.Backend.Geofencing
}
}
reconciledGeofencingData := make(map[string][]string)
if geofencingAPI := distribution.Config.Backend.HttpBackend.Geofencing; geofencingAPI != nil && len(*geofencingAPI) > 0 {
newGeofencingMap := *geofencingAPI
for url, newCountries := range newGeofencingMap {
oldCountriesPtrs := oldGeofencingMap[url]
oldCountries := utils.ConvertPointerSliceToStringSlice(oldCountriesPtrs)
reconciledCountries := utils.ReconcileStringSlices(oldCountries, newCountries)
reconciledGeofencingData[url] = reconciledCountries
}
}
geofencingVal := types.MapNull(geofencingTypes.ElemType)
if len(reconciledGeofencingData) > 0 {
geofencingMapElems := make(map[string]attr.Value)
for url, countries := range reconciledGeofencingData {
listVal, diags := types.ListValueFrom(ctx, types.StringType, countries)
if diags.HasError() {
return core.DiagsToError(diags)
}
geofencingMapElems[url] = listVal
}
var mappedGeofencing basetypes.MapValue
mappedGeofencing, diags = types.MapValue(geofencingTypes.ElemType, geofencingMapElems)
if diags.HasError() {
return core.DiagsToError(diags)
}
geofencingVal = mappedGeofencing
}
// note that httpbackend is hardcoded here as long as it is the only available backend
backend, diags := types.ObjectValue(backendTypes, map[string]attr.Value{
"type": types.StringValue(*distribution.Config.Backend.HttpBackend.Type),
"origin_url": types.StringValue(*distribution.Config.Backend.HttpBackend.OriginUrl),
"origin_request_headers": originRequestHeaders,
"geofencing": geofencingVal,
})
if diags.HasError() {
return core.DiagsToError(diags)
@ -678,6 +796,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution
Regions: cfg.Regions,
BlockedCountries: cfg.BlockedCountries,
OriginRequestHeaders: cfg.Backend.HttpBackend.OriginRequestHeaders,
Geofencing: cfg.Backend.HttpBackend.Geofencing,
Optimizer: optimizer,
}
@ -722,6 +841,25 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
}
}
// geofencing
geofencing := map[string][]string{}
if configModel.Backend.Geofencing != nil {
for endpoint, countryCodes := range *configModel.Backend.Geofencing {
geofencingCountry := make([]string, len(countryCodes))
for i, countryCodePtr := range countryCodes {
if countryCodePtr == nil {
return nil, fmt.Errorf("geofencing url %q has a null value", endpoint)
}
validatedCountry, err := validateCountryCode(*countryCodePtr)
if err != nil {
return nil, err
}
geofencingCountry[i] = validatedCountry
}
geofencing[endpoint] = geofencingCountry
}
}
// originRequestHeaders
originRequestHeaders := map[string]string{}
if configModel.Backend.OriginRequestHeaders != nil {
@ -736,6 +874,7 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
OriginRequestHeaders: &originRequestHeaders,
OriginUrl: &configModel.Backend.OriginURL,
Type: &configModel.Backend.Type,
Geofencing: &geofencing,
},
},
Regions: &regions,

View file

@ -17,10 +17,18 @@ func TestToCreatePayload(t *testing.T) {
"testHeader1": types.StringValue("testHeaderValue1"),
}
originRequestHeaders := types.MapValueMust(types.StringType, headers)
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("DE"),
types.StringValue("FR"),
})
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
"https://de.mycoolapp.com": geofencingCountries,
})
backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{
"type": types.StringValue("http"),
"origin_url": types.StringValue("https://www.mycoolapp.com"),
"origin_request_headers": originRequestHeaders,
"geofencing": geofencing,
})
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
regionsFixture := types.ListValueMust(types.StringType, regions)
@ -61,6 +69,9 @@ func TestToCreatePayload(t *testing.T) {
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
Regions: &[]cdn.Region{"EU", "US"},
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
Geofencing: &map[string][]string{
"https://de.mycoolapp.com": {"DE", "FR"},
},
},
IsValid: true,
},
@ -82,6 +93,9 @@ func TestToCreatePayload(t *testing.T) {
Regions: &[]cdn.Region{"EU", "US"},
Optimizer: cdn.NewOptimizer(true),
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
Geofencing: &map[string][]string{
"https://de.mycoolapp.com": {"DE", "FR"},
},
},
IsValid: true,
},
@ -126,10 +140,18 @@ func TestConvertConfig(t *testing.T) {
"testHeader1": types.StringValue("testHeaderValue1"),
}
originRequestHeaders := types.MapValueMust(types.StringType, headers)
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("DE"),
types.StringValue("FR"),
})
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
"https://de.mycoolapp.com": geofencingCountries,
})
backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{
"type": types.StringValue("http"),
"origin_url": types.StringValue("https://www.mycoolapp.com"),
"origin_request_headers": originRequestHeaders,
"geofencing": geofencing,
})
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
regionsFixture := types.ListValueMust(types.StringType, regions)
@ -169,6 +191,9 @@ func TestConvertConfig(t *testing.T) {
},
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
Type: cdn.PtrString("http"),
Geofencing: &map[string][]string{
"https://de.mycoolapp.com": {"DE", "FR"},
},
},
},
Regions: &[]cdn.Region{"EU", "US"},
@ -194,6 +219,9 @@ func TestConvertConfig(t *testing.T) {
},
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
Type: cdn.PtrString("http"),
Geofencing: &map[string][]string{
"https://de.mycoolapp.com": {"DE", "FR"},
},
},
},
Regions: &[]cdn.Region{"EU", "US"},
@ -246,11 +274,17 @@ func TestMapFields(t *testing.T) {
"type": types.StringValue("http"),
"origin_url": types.StringValue("https://www.mycoolapp.com"),
"origin_request_headers": originRequestHeaders,
"geofencing": types.MapNull(geofencingTypes.ElemType),
})
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)
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("DE"), types.StringValue("BR")})
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
"test/": geofencingCountries,
})
geofencingInput := map[string][]string{"test/": {"DE", "BR"}}
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{
"enabled": types.BoolValue(true),
})
@ -347,6 +381,26 @@ func TestMapFields(t *testing.T) {
}),
IsValid: true,
},
"happy_path_with_geofencing": {
Expected: expectedModel(func(m *Model) {
backendWithGeofencing := types.ObjectValueMust(backendTypes, map[string]attr.Value{
"type": types.StringValue("http"),
"origin_url": types.StringValue("https://www.mycoolapp.com"),
"origin_request_headers": originRequestHeaders,
"geofencing": geofencing,
})
m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
"backend": backendWithGeofencing,
"regions": regionsFixture,
"optimizer": types.ObjectNull(optimizerTypes),
"blocked_countries": blockedCountriesFixture,
})
}),
Input: distributionFixture(func(d *cdn.Distribution) {
d.Config.Backend.HttpBackend.Geofencing = &geofencingInput
}),
IsValid: true,
},
"happy_path_status_error": {
Expected: expectedModel(func(m *Model) {
m.Status = types.StringValue("ERROR")
@ -412,7 +466,7 @@ func TestMapFields(t *testing.T) {
for tn, tc := range tests {
t.Run(tn, func(t *testing.T) {
model := &Model{}
err := mapFields(tc.Input, model)
err := mapFields(context.Background(), tc.Input, model)
if err != nil && tc.IsValid {
t.Fatalf("Error mapping fields: %v", err)
}

View file

@ -95,6 +95,19 @@ func SimplifyBackupSchedule(schedule string) string {
return simplifiedSchedule
}
// ConvertPointerSliceToStringSlice safely converts a slice of string pointers to a slice of strings.
func ConvertPointerSliceToStringSlice(pointerSlice []*string) []string {
if pointerSlice == nil {
return []string{}
}
stringSlice := make([]string, 0, len(pointerSlice))
for _, strPtr := range pointerSlice {
if strPtr != nil { // Safely skip any nil pointers in the list
stringSlice = append(stringSlice, *strPtr)
}
}
return stringSlice
}
func SupportedValuesDocumentation(values []string) string {
if len(values) == 0 {
return ""

View file

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
)
func TestReconcileStrLists(t *testing.T) {
@ -128,6 +129,55 @@ func TestListValuetoStrSlice(t *testing.T) {
}
}
func TestConvertPointerSliceToStringSlice(t *testing.T) {
tests := []struct {
description string
input []*string
expected []string
}{
{
description: "nil slice",
input: nil,
expected: []string{},
},
{
description: "empty slice",
input: []*string{},
expected: []string{},
},
{
description: "slice with valid pointers",
input: []*string{utils.Ptr("apple"), utils.Ptr("banana"), utils.Ptr("cherry")},
expected: []string{"apple", "banana", "cherry"},
},
{
description: "slice with some nil pointers",
input: []*string{utils.Ptr("apple"), nil, utils.Ptr("cherry"), nil},
expected: []string{"apple", "cherry"},
},
{
description: "slice with all nil pointers",
input: []*string{nil, nil, nil},
expected: []string{},
},
{
description: "slice with a pointer to an empty string",
input: []*string{utils.Ptr("apple"), utils.Ptr(""), utils.Ptr("cherry")},
expected: []string{"apple", "", "cherry"},
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output := ConvertPointerSliceToStringSlice(tt.input)
diff := cmp.Diff(output, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestSimplifyBackupSchedule(t *testing.T) {
tests := []struct {
description string