Skip to content

Commit af17e1c

Browse files
author
Matheus Politano
committed
add CDN geofencing
1 parent f0438e8 commit af17e1c

File tree

7 files changed

+136
-4
lines changed

7 files changed

+136
-4
lines changed

docs/data-sources/cdn_distribution.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ Read-Only:
5656
<a id="nestedatt--config--backend"></a>
5757
### Nested Schema for `config.backend`
5858

59+
Optional:
60+
61+
- `geofencing` (Map of List of String) A map of URLs to a list of countries where content is allowed.
62+
5963
Read-Only:
6064

6165
- `origin_request_headers` (Map of String) The configured origin request headers for the backend

docs/resources/cdn_distribution.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Required:
8080

8181
Optional:
8282

83+
- `geofencing` (Map of List of String) A map of URLs to a list of countries where content is allowed.
8384
- `origin_request_headers` (Map of String) The configured origin request headers for the backend
8485

8586

examples/resources/stackit_cdn_distribution/resource.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ resource "stackit_cdn_distribution" "example_distribution" {
44
backend = {
55
type = "http"
66
origin_url = "https://mybackend.onstackit.cloud"
7+
geofencing = {
8+
"https://mybackend.onstackit.cloud" = ["DE"]
9+
}
710
}
811
regions = ["EU", "US", "ASIA", "AF", "SA"]
912
blocked_countries = ["DE", "AT", "CH"]

stackit/internal/services/cdn/cdn_acc_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ func configResources(regions string) string {
4545
backend = {
4646
type = "http"
4747
origin_url = "%s"
48+
geofencing = {
49+
"%s" = ["DE"]
50+
}
4851
}
4952
regions = [%s]
5053
blocked_countries = [%s]
@@ -70,7 +73,7 @@ func configResources(regions string) string {
7073
type = "CNAME"
7174
records = ["${stackit_cdn_distribution.distribution.domains[0].name}."]
7275
}
73-
`, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"],
76+
`, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], instanceResource["config_backend_origin_url"],
7477
regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"],
7578
testutil.ProjectId, instanceResource["custom_domain_prefix"])
7679
}
@@ -173,6 +176,11 @@ func TestAccCDNDistributionResource(t *testing.T) {
173176
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"),
174177
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"),
175178
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"),
179+
resource.TestCheckResourceAttr(
180+
"stackit_cdn_distribution.distribution",
181+
fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]),
182+
"DE",
183+
),
176184
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"),
177185
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
178186
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),

stackit/internal/services/cdn/distribution/datasource.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe
142142
Description: schemaDescriptions["config_backend_origin_request_headers"],
143143
ElementType: types.StringType,
144144
},
145+
"geofencing": schema.MapAttribute{
146+
Description: "A map of URLs to a list of countries where content is allowed.",
147+
Optional: true,
148+
ElementType: types.ListType{
149+
ElemType: types.StringType,
150+
},
151+
},
145152
},
146153
},
147154
"regions": schema.ListAttribute{

stackit/internal/services/cdn/distribution/resource.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,10 @@ type optimizerConfig struct {
8888
}
8989

9090
type backend struct {
91-
Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" backend is supported
92-
OriginURL string `tfsdk:"origin_url"` // The origin URL of the backend
93-
OriginRequestHeaders *map[string]string `tfsdk:"origin_request_headers"` // Request headers that should be added by the CDN distribution to incoming requests
91+
Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" backend is supported
92+
OriginURL string `tfsdk:"origin_url"` // The origin URL of the backend
93+
OriginRequestHeaders *map[string]string `tfsdk:"origin_request_headers"` // Request headers that should be added by the CDN distribution to incoming requests
94+
Geofencing *map[string][]string `tfsdk:"geofencing"` // The geofencing is an object mapping multiple alternative origins to country codes.
9495
}
9596

9697
var configTypes = map[string]attr.Type{
@@ -106,10 +107,15 @@ var optimizerTypes = map[string]attr.Type{
106107
"enabled": types.BoolType,
107108
}
108109

110+
var geofencingTypes = types.MapType{ElemType: types.ListType{
111+
ElemType: types.StringType,
112+
}}
113+
109114
var backendTypes = map[string]attr.Type{
110115
"type": types.StringType,
111116
"origin_url": types.StringType,
112117
"origin_request_headers": types.MapType{ElemType: types.StringType},
118+
"geofencing": geofencingTypes,
113119
}
114120

115121
var domainTypes = map[string]attr.Type{
@@ -256,6 +262,13 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques
256262
Description: schemaDescriptions["config_backend_origin_request_headers"],
257263
ElementType: types.StringType,
258264
},
265+
"geofencing": schema.MapAttribute{
266+
Description: "A map of URLs to a list of countries where content is allowed.",
267+
Optional: true,
268+
ElementType: types.ListType{
269+
ElemType: types.StringType,
270+
},
271+
},
259272
},
260273
},
261274
"regions": schema.ListAttribute{
@@ -414,6 +427,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe
414427
OriginRequestHeaders: configModel.Backend.OriginRequestHeaders,
415428
OriginUrl: &configModel.Backend.OriginURL,
416429
Type: &configModel.Backend.Type,
430+
Geofencing: configModel.Backend.Geofencing,
417431
},
418432
},
419433
Regions: &regions,
@@ -584,11 +598,34 @@ func mapFields(distribution *cdn.Distribution, model *Model) error {
584598
return core.DiagsToError(diags)
585599
}
586600
}
601+
// geofencing
602+
geofencingVal := types.MapNull(geofencingTypes.ElemType)
603+
if geofencingAPI := distribution.Config.Backend.HttpBackend.Geofencing; geofencingAPI != nil && len(*geofencingAPI) > 0 {
604+
geofencingMap := make(map[string]attr.Value)
605+
for url, countries := range *geofencingAPI {
606+
countryVals := []attr.Value{}
607+
for _, country := range countries {
608+
countryVals = append(countryVals, types.StringValue(country))
609+
}
610+
listVal, diags := types.ListValue(types.StringType, countryVals)
611+
if diags.HasError() {
612+
return core.DiagsToError(diags)
613+
}
614+
geofencingMap[url] = listVal
615+
}
616+
617+
mappedGeofencing, diags := types.MapValue(geofencingTypes.ElemType, geofencingMap)
618+
if diags.HasError() {
619+
return core.DiagsToError(diags)
620+
}
621+
geofencingVal = mappedGeofencing
622+
}
587623
// note that httpbackend is hardcoded here as long as it is the only available backend
588624
backend, diags := types.ObjectValue(backendTypes, map[string]attr.Value{
589625
"type": types.StringValue(*distribution.Config.Backend.HttpBackend.Type),
590626
"origin_url": types.StringValue(*distribution.Config.Backend.HttpBackend.OriginUrl),
591627
"origin_request_headers": originRequestHeaders,
628+
"geofencing": geofencingVal,
592629
})
593630
if diags.HasError() {
594631
return core.DiagsToError(diags)
@@ -678,6 +715,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution
678715
Regions: cfg.Regions,
679716
BlockedCountries: cfg.BlockedCountries,
680717
OriginRequestHeaders: cfg.Backend.HttpBackend.OriginRequestHeaders,
718+
Geofencing: cfg.Backend.HttpBackend.Geofencing,
681719
Optimizer: optimizer,
682720
}
683721

@@ -722,6 +760,22 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
722760
}
723761
}
724762

763+
// geofencing
764+
geofencing := map[string][]string{}
765+
if configModel.Backend.Geofencing != nil {
766+
for endpoint, countryCodes := range *configModel.Backend.Geofencing {
767+
geofencingContry := make([]string, len(countryCodes))
768+
for i, countryCode := range countryCodes {
769+
validatedBlockedCountry, err := validateCountryCode(countryCode)
770+
if err != nil {
771+
return nil, err
772+
}
773+
geofencingContry[i] = validatedBlockedCountry
774+
}
775+
geofencing[endpoint] = geofencingContry
776+
}
777+
}
778+
725779
// originRequestHeaders
726780
originRequestHeaders := map[string]string{}
727781
if configModel.Backend.OriginRequestHeaders != nil {
@@ -736,6 +790,7 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
736790
OriginRequestHeaders: &originRequestHeaders,
737791
OriginUrl: &configModel.Backend.OriginURL,
738792
Type: &configModel.Backend.Type,
793+
Geofencing: &geofencing,
739794
},
740795
},
741796
Regions: &regions,

stackit/internal/services/cdn/distribution/resource_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,18 @@ func TestToCreatePayload(t *testing.T) {
1717
"testHeader1": types.StringValue("testHeaderValue1"),
1818
}
1919
originRequestHeaders := types.MapValueMust(types.StringType, headers)
20+
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{
21+
types.StringValue("DE"),
22+
types.StringValue("FR"),
23+
})
24+
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
25+
"https://de.mycoolapp.com": geofencingCountries,
26+
})
2027
backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{
2128
"type": types.StringValue("http"),
2229
"origin_url": types.StringValue("https://www.mycoolapp.com"),
2330
"origin_request_headers": originRequestHeaders,
31+
"geofencing": geofencing,
2432
})
2533
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
2634
regionsFixture := types.ListValueMust(types.StringType, regions)
@@ -61,6 +69,9 @@ func TestToCreatePayload(t *testing.T) {
6169
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
6270
Regions: &[]cdn.Region{"EU", "US"},
6371
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
72+
Geofencing: &map[string][]string{
73+
"https://de.mycoolapp.com": {"DE", "FR"},
74+
},
6475
},
6576
IsValid: true,
6677
},
@@ -82,6 +93,9 @@ func TestToCreatePayload(t *testing.T) {
8293
Regions: &[]cdn.Region{"EU", "US"},
8394
Optimizer: cdn.NewOptimizer(true),
8495
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
96+
Geofencing: &map[string][]string{
97+
"https://de.mycoolapp.com": {"DE", "FR"},
98+
},
8599
},
86100
IsValid: true,
87101
},
@@ -126,10 +140,18 @@ func TestConvertConfig(t *testing.T) {
126140
"testHeader1": types.StringValue("testHeaderValue1"),
127141
}
128142
originRequestHeaders := types.MapValueMust(types.StringType, headers)
143+
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{
144+
types.StringValue("DE"),
145+
types.StringValue("FR"),
146+
})
147+
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
148+
"https://de.mycoolapp.com": geofencingCountries,
149+
})
129150
backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{
130151
"type": types.StringValue("http"),
131152
"origin_url": types.StringValue("https://www.mycoolapp.com"),
132153
"origin_request_headers": originRequestHeaders,
154+
"geofencing": geofencing,
133155
})
134156
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
135157
regionsFixture := types.ListValueMust(types.StringType, regions)
@@ -169,6 +191,9 @@ func TestConvertConfig(t *testing.T) {
169191
},
170192
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
171193
Type: cdn.PtrString("http"),
194+
Geofencing: &map[string][]string{
195+
"https://de.mycoolapp.com": {"DE", "FR"},
196+
},
172197
},
173198
},
174199
Regions: &[]cdn.Region{"EU", "US"},
@@ -194,6 +219,9 @@ func TestConvertConfig(t *testing.T) {
194219
},
195220
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
196221
Type: cdn.PtrString("http"),
222+
Geofencing: &map[string][]string{
223+
"https://de.mycoolapp.com": {"DE", "FR"},
224+
},
197225
},
198226
},
199227
Regions: &[]cdn.Region{"EU", "US"},
@@ -246,11 +274,17 @@ func TestMapFields(t *testing.T) {
246274
"type": types.StringValue("http"),
247275
"origin_url": types.StringValue("https://www.mycoolapp.com"),
248276
"origin_request_headers": originRequestHeaders,
277+
"geofencing": types.MapNull(geofencingTypes.ElemType),
249278
})
250279
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
251280
regionsFixture := types.ListValueMust(types.StringType, regions)
252281
blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")}
253282
blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries)
283+
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("DE"), types.StringValue("BR")})
284+
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
285+
"test/": geofencingCountries,
286+
})
287+
geofencingInput := map[string][]string{"test/": {"DE", "BR"}}
254288
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{
255289
"enabled": types.BoolValue(true),
256290
})
@@ -347,6 +381,26 @@ func TestMapFields(t *testing.T) {
347381
}),
348382
IsValid: true,
349383
},
384+
"happy_path_with_geofencing": {
385+
Expected: expectedModel(func(m *Model) {
386+
backendWithGeofencing := types.ObjectValueMust(backendTypes, map[string]attr.Value{
387+
"type": types.StringValue("http"),
388+
"origin_url": types.StringValue("https://www.mycoolapp.com"),
389+
"origin_request_headers": originRequestHeaders,
390+
"geofencing": geofencing,
391+
})
392+
m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
393+
"backend": backendWithGeofencing,
394+
"regions": regionsFixture,
395+
"optimizer": types.ObjectNull(optimizerTypes),
396+
"blocked_countries": blockedCountriesFixture,
397+
})
398+
}),
399+
Input: distributionFixture(func(d *cdn.Distribution) {
400+
d.Config.Backend.HttpBackend.Geofencing = &geofencingInput
401+
}),
402+
IsValid: true,
403+
},
350404
"happy_path_status_error": {
351405
Expected: expectedModel(func(m *Model) {
352406
m.Status = types.StringValue("ERROR")

0 commit comments

Comments
 (0)