Skip to content

Commit 8d6e86b

Browse files
feat(cdn): implement new bucket backend type (#1185)
relates to https://jira.schwarz/browse/STACKITCDN-1075
1 parent 9edafb2 commit 8d6e86b

8 files changed

Lines changed: 1140 additions & 113 deletions

File tree

docs/data-sources/cdn_distribution.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ Read-Only:
5858

5959
Read-Only:
6060

61-
- `geofencing` (Map of List of String) A map of URLs to a list of countries where content is allowed.
62-
- `origin_request_headers` (Map of String) The configured origin request headers for the backend
63-
- `origin_url` (String) The configured backend type for the distribution
64-
- `type` (String) The configured backend type. Possible values are: `http`.
61+
- `bucket_url` (String) The URL of the bucket (e.g. https://s3.example.com). Required if type is 'bucket'.
62+
- `geofencing` (Map of List of String) The configured type http to configure countries where content is allowed. A map of URLs to a list of countries
63+
- `origin_request_headers` (Map of String) The configured type http origin request headers for the backend
64+
- `origin_url` (String) The configured backend type http for the distribution
65+
- `region` (String) The region where the bucket is hosted. Required if type is 'bucket'.
66+
- `type` (String) The configured backend type. Possible values are: `http`, `bucket`.
6567

6668

6769
<a id="nestedatt--config--optimizer"></a>

docs/resources/cdn_distribution.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ resource "stackit_cdn_distribution" "example_distribution" {
3535
}
3636
}
3737
38+
resource "stackit_cdn_distribution" "example_bucket_distribution" {
39+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
40+
config = {
41+
backend = {
42+
type = "bucket"
43+
bucket_url = "https://my-test.object.storage.eu01.onstackit.cloud"
44+
region = "eu01"
45+
46+
# Credentials are required for bucket backends
47+
# It is strongly recommended to use variables for secrets
48+
credentials = {
49+
access_key_id = var.bucket_access_key
50+
secret_access_key = var.bucket_secret_key
51+
}
52+
}
53+
regions = ["EU", "US"]
54+
blocked_countries = ["CN", "RU"]
55+
56+
optimizer = {
57+
enabled = false
58+
}
59+
}
60+
}
61+
3862
# Only use the import statement, if you want to import an existing cdn distribution
3963
import {
4064
to = stackit_cdn_distribution.import-example
@@ -78,13 +102,25 @@ Optional:
78102

79103
Required:
80104

81-
- `origin_url` (String) The configured backend type for the distribution
82-
- `type` (String) The configured backend type. Possible values are: `http`.
105+
- `type` (String) The configured backend type. Possible values are: `http`, `bucket`.
83106

84107
Optional:
85108

86-
- `geofencing` (Map of List of String) A map of URLs to a list of countries where content is allowed.
87-
- `origin_request_headers` (Map of String) The configured origin request headers for the backend
109+
- `bucket_url` (String) The URL of the bucket (e.g. https://s3.example.com). Required if type is 'bucket'.
110+
- `credentials` (Attributes) The credentials for the bucket. Required if type is 'bucket'. (see [below for nested schema](#nestedatt--config--backend--credentials))
111+
- `geofencing` (Map of List of String) The configured type http to configure countries where content is allowed. A map of URLs to a list of countries
112+
- `origin_request_headers` (Map of String) The configured type http origin request headers for the backend
113+
- `origin_url` (String) The configured backend type http for the distribution
114+
- `region` (String) The region where the bucket is hosted. Required if type is 'bucket'.
115+
116+
<a id="nestedatt--config--backend--credentials"></a>
117+
### Nested Schema for `config.backend.credentials`
118+
119+
Required:
120+
121+
- `access_key_id` (String, Sensitive) The access key for the bucket. Required if type is 'bucket'.
122+
- `secret_access_key` (String, Sensitive) The access key for the bucket. Required if type is 'bucket'.
123+
88124

89125

90126
<a id="nestedatt--config--optimizer"></a>

examples/resources/stackit_cdn_distribution/resource.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ resource "stackit_cdn_distribution" "example_distribution" {
1717
}
1818
}
1919

20+
resource "stackit_cdn_distribution" "example_bucket_distribution" {
21+
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
22+
config = {
23+
backend = {
24+
type = "bucket"
25+
bucket_url = "https://my-test.object.storage.eu01.onstackit.cloud"
26+
region = "eu01"
27+
28+
# Credentials are required for bucket backends
29+
# It is strongly recommended to use variables for secrets
30+
credentials = {
31+
access_key_id = var.bucket_access_key
32+
secret_access_key = var.bucket_secret_key
33+
}
34+
}
35+
regions = ["EU", "US"]
36+
blocked_countries = ["CN", "RU"]
37+
38+
optimizer = {
39+
enabled = false
40+
}
41+
}
42+
}
43+
2044
# Only use the import statement, if you want to import an existing cdn distribution
2145
import {
2246
to = stackit_cdn_distribution.import-example

stackit/internal/services/cdn/cdn_acc_test.go

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"time"
1616

1717
"github.com/google/uuid"
18+
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
1819
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1920
"github.com/hashicorp/terraform-plugin-testing/terraform"
2021
"github.com/stackitcloud/stackit-sdk-go/core/config"
@@ -357,6 +358,182 @@ func TestAccCDNDistributionResource(t *testing.T) {
357358
},
358359
})
359360
}
361+
362+
func configBucketResources(bucketName, credentialsGroupName string) string {
363+
return fmt.Sprintf(`
364+
%s
365+
366+
resource "stackit_objectstorage_bucket" "bucket" {
367+
project_id = "%s"
368+
name = "%s"
369+
}
370+
371+
resource "stackit_objectstorage_credentials_group" "group" {
372+
project_id = "%s"
373+
name = "%s"
374+
}
375+
376+
resource "stackit_objectstorage_credential" "creds" {
377+
project_id = "%s"
378+
credentials_group_id = stackit_objectstorage_credentials_group.group.credentials_group_id
379+
}
380+
381+
resource "stackit_cdn_distribution" "distribution" {
382+
project_id = "%s"
383+
config = {
384+
backend = {
385+
type = "bucket"
386+
# Construct the URL dynamically using the bucket name
387+
bucket_url = "https://${stackit_objectstorage_bucket.bucket.name}.object.storage.eu01.onstackit.cloud"
388+
region = "eu01"
389+
390+
# Pass the keys via credentials block
391+
credentials = {
392+
access_key_id = stackit_objectstorage_credential.creds.access_key
393+
secret_access_key = stackit_objectstorage_credential.creds.secret_access_key
394+
}
395+
}
396+
regions = ["EU", "US"]
397+
blocked_countries = ["CN", "RU"]
398+
399+
optimizer = {
400+
enabled = false
401+
}
402+
}
403+
}
404+
`, testutil.CdnProviderConfig(),
405+
testutil.ProjectId, bucketName,
406+
testutil.ProjectId, credentialsGroupName,
407+
testutil.ProjectId,
408+
testutil.ProjectId,
409+
)
410+
}
411+
412+
func configBucketDatasource(bucketName, credentialsGroupName string) string {
413+
return fmt.Sprintf(`
414+
%s
415+
416+
data "stackit_cdn_distribution" "bucket_ds" {
417+
project_id = stackit_cdn_distribution.distribution.project_id
418+
distribution_id = stackit_cdn_distribution.distribution.distribution_id
419+
}
420+
`, configBucketResources(bucketName, credentialsGroupName))
421+
}
422+
423+
func TestAccCDNDistributionBucketResource(t *testing.T) {
424+
bucketName := fmt.Sprintf("tf-acc-bucket-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
425+
credentialsGroupName := fmt.Sprintf("tf-acc-group-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
426+
bucketNameUpdated := fmt.Sprintf("tf-acc-bucket-upd-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
427+
credentialsGroupNameUpdated := fmt.Sprintf("tf-acc-group-upd-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
428+
429+
expectedBucketUrl := fmt.Sprintf("https://%s.object.storage.eu01.onstackit.cloud", bucketName)
430+
expectedBucketUrlUpdated := fmt.Sprintf("https://%s.object.storage.eu01.onstackit.cloud", bucketNameUpdated)
431+
432+
resource.Test(t, resource.TestCase{
433+
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
434+
CheckDestroy: testAccCheckCDNDistributionDestroy,
435+
Steps: []resource.TestStep{
436+
// Step 1: Create Resource (Real Bucket & Creds)
437+
{
438+
Config: configBucketResources(bucketName, credentialsGroupName),
439+
Check: resource.ComposeAggregateTestCheckFunc(
440+
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"),
441+
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"),
442+
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"),
443+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
444+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
445+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "1"),
446+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
447+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
448+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "2"),
449+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"),
450+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"),
451+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"),
452+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CN"),
453+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "RU"),
454+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "false"),
455+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.type", "bucket"),
456+
457+
// Verify the Bucket URL matches the one we constructed
458+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.bucket_url", expectedBucketUrl),
459+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.region", "eu01"),
460+
461+
// CRITICAL: Verify that the CDN keys match the Object Storage keys
462+
// We use AttrPair because the values are generated dynamically on the server side
463+
resource.TestCheckResourceAttrPair(
464+
"stackit_cdn_distribution.distribution", "config.backend.credentials.access_key_id",
465+
"stackit_objectstorage_credential.creds", "access_key",
466+
),
467+
resource.TestCheckResourceAttrPair(
468+
"stackit_cdn_distribution.distribution", "config.backend.credentials.secret_access_key",
469+
"stackit_objectstorage_credential.creds", "secret_access_key",
470+
),
471+
),
472+
},
473+
{
474+
ResourceName: "stackit_cdn_distribution.distribution",
475+
ImportStateIdFunc: func(s *terraform.State) (string, error) {
476+
r, ok := s.RootModule().Resources["stackit_cdn_distribution.distribution"]
477+
if !ok {
478+
return "", fmt.Errorf("couldn't find resource")
479+
}
480+
return fmt.Sprintf("%s,%s", testutil.ProjectId, r.Primary.Attributes["distribution_id"]), nil
481+
},
482+
ImportState: true,
483+
ImportStateVerify: true,
484+
// We MUST ignore credentials on import verification
485+
// 1. API doesn't return them (security).
486+
// 2. State has them (from resource creation).
487+
ImportStateVerifyIgnore: []string{"config.backend.credentials"},
488+
},
489+
// Step 3: Data Source
490+
{
491+
Config: configBucketDatasource(bucketName, credentialsGroupName),
492+
Check: resource.ComposeAggregateTestCheckFunc(
493+
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.bucket_ds", "distribution_id"),
494+
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.bucket_ds", "created_at"),
495+
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.bucket_ds", "updated_at"),
496+
resource.TestCheckResourceAttrPair("data.stackit_cdn_distribution.bucket_ds", "project_id", "stackit_cdn_distribution.distribution", "project_id"),
497+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "status", "ACTIVE"),
498+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "domains.#", "1"),
499+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "domains.0.type", "managed"),
500+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "domains.0.status", "ACTIVE"),
501+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.regions.#", "2"),
502+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.regions.0", "EU"),
503+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.regions.1", "US"),
504+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.blocked_countries.#", "2"),
505+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.blocked_countries.0", "CN"),
506+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.blocked_countries.1", "RU"),
507+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.optimizer.enabled", "false"),
508+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.type", "bucket"),
509+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.bucket_url", expectedBucketUrl),
510+
511+
// Security Check: Secrets should NOT be in Data Source
512+
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.credentials.access_key_id"),
513+
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.credentials.secret_access_key"),
514+
),
515+
},
516+
// Step 4: Update Resource (Change Bucket & Creds)
517+
{
518+
Config: configBucketResources(bucketNameUpdated, credentialsGroupNameUpdated),
519+
Check: resource.ComposeAggregateTestCheckFunc(
520+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
521+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.bucket_url", expectedBucketUrlUpdated),
522+
523+
// Verify that keys have been updated to the new credentials
524+
resource.TestCheckResourceAttrPair(
525+
"stackit_cdn_distribution.distribution", "config.backend.credentials.access_key_id",
526+
"stackit_objectstorage_credential.creds", "access_key",
527+
),
528+
resource.TestCheckResourceAttrPair(
529+
"stackit_cdn_distribution.distribution", "config.backend.credentials.secret_access_key",
530+
"stackit_objectstorage_credential.creds", "secret_access_key",
531+
),
532+
),
533+
},
534+
},
535+
})
536+
}
360537
func testAccCheckCDNDistributionDestroy(s *terraform.State) error {
361538
ctx := context.Background()
362539
var client *cdn.APIClient
@@ -400,9 +577,26 @@ const (
400577
)
401578

402579
func blockUntilDomainResolves(domain string) (net.IP, error) {
580+
// Create a custom resolver that bypasses the local system DNS settings/cache
581+
// and queries Google DNS (8.8.8.8) directly.
582+
r := &net.Resolver{
583+
PreferGo: true,
584+
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
585+
d := net.Dialer{
586+
Timeout: time.Millisecond * time.Duration(10000),
587+
}
588+
// Force query to Google DNS
589+
return d.DialContext(ctx, network, "8.8.8.8:53")
590+
},
591+
}
592+
403593
// wait until it becomes ready
404594
isReady := func() (net.IP, error) {
405-
ips, err := net.LookupIP(domain)
595+
// Use a context for the individual query timeout
596+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
597+
defer cancel()
598+
599+
ips, err := r.LookupIP(ctx, "ip", domain)
406600
if err != nil {
407601
return nil, fmt.Errorf("error looking up IP for domain %s: %w", domain, err)
408602
}
@@ -413,6 +607,7 @@ func blockUntilDomainResolves(domain string) (net.IP, error) {
413607
}
414608
return nil, fmt.Errorf("no IP for domain: %v", domain)
415609
}
610+
416611
return retry(recordCheckAttempts, recordCheckInterval, isReady)
417612
}
418613

0 commit comments

Comments
 (0)