Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a69e3cb
chore: add new field and add them in map field
Feb 2, 2026
877ff6a
Add create payload
Feb 2, 2026
6a1bb3a
add update
Feb 2, 2026
16285e7
add all changes in resource
Feb 2, 2026
26f3f8e
add resource test
Feb 3, 2026
e3f7364
add bucket in datasource
Feb 3, 2026
00aa525
implement a new logic to blockUntilDomainResolves
Feb 3, 2026
1c250c0
implement own mapDataSourceFields to data source
Feb 3, 2026
37d5712
add new cdn acc test
Feb 3, 2026
4e7bf7c
generate docs
Feb 3, 2026
669b495
add example
Feb 3, 2026
5693a84
Merge branch 'main' into mp/feat/integrate-bucket
matheuspolitano Feb 4, 2026
f7bd2d6
Merge branch 'main' into mp/feat/integrate-bucket
matheuspolitano Feb 4, 2026
276913b
add more validations
Feb 4, 2026
1a272ea
create a test to new mapdatasource function
Feb 4, 2026
152e430
improve doc
Feb 4, 2026
ae4dd60
update docs
Feb 4, 2026
5c279bb
fix miss spelling word
Feb 9, 2026
a9c3b70
Merge branch 'main' into mp/feat/integrate-bucket
Feb 9, 2026
8af58af
fix lint issues
Feb 10, 2026
68a194a
rename the bucket access_key to access_key_id
Feb 11, 2026
10bfd32
chore: use the isUdefiened util function
Feb 11, 2026
fe543ee
chore: remove leftover comments
Feb 11, 2026
9bf58dd
add datasource test and bucket
Feb 11, 2026
9045687
fix lint
Feb 11, 2026
493beb8
feat: add credential field
Feb 11, 2026
ff5e0a9
misc: improve the doc
Feb 11, 2026
ce6ad9f
chore: add example
Feb 12, 2026
f7faa95
chore: doc update
Feb 12, 2026
2653272
feat: check more fields
Feb 12, 2026
b06d2d2
feat: add update test
Feb 12, 2026
5e9ca7d
chore: remove comment
Feb 12, 2026
2f24a5e
Merge branch 'main' into mp/feat/integrate-bucket
matheuspolitano Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/data-sources/cdn_distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ 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. Possible values are: `http`.
- `bucket_url` (String) The URL of the bucket (e.g. https://s3.example.com). Required if type is 'bucket'.
- `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
- `origin_request_headers` (Map of String) The configured type http origin request headers for the backend
- `origin_url` (String) The configured backend type http for the distribution
- `region` (String) The region where the bucket is hosted. Required if type is 'bucket'.
- `type` (String) The configured backend type. Possible values are: `http`, `bucket`.


<a id="nestedatt--config--optimizer"></a>
Expand Down
44 changes: 40 additions & 4 deletions docs/resources/cdn_distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ resource "stackit_cdn_distribution" "example_distribution" {
}
}

resource "stackit_cdn_distribution" "example_bucket_distribution" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
config = {
backend = {
type = "bucket"
bucket_url = "https://my-test.object.storage.eu01.onstackit.cloud"
region = "eu01"

# Credentials are required for bucket backends
# It is strongly recommended to use variables for secrets
credentials = {
access_key_id = var.bucket_access_key
secret_access_key = var.bucket_secret_key
}
}
regions = ["EU", "US"]
blocked_countries = ["CN", "RU"]

optimizer = {
enabled = false
}
}
}

# Only use the import statement, if you want to import an existing cdn distribution
import {
to = stackit_cdn_distribution.import-example
Expand Down Expand Up @@ -78,13 +102,25 @@ Optional:

Required:

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

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
- `bucket_url` (String) The URL of the bucket (e.g. https://s3.example.com). Required if type is 'bucket'.
- `credentials` (Attributes) The credentials for the bucket. Required if type is 'bucket'. (see [below for nested schema](#nestedatt--config--backend--credentials))
- `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
- `origin_request_headers` (Map of String) The configured type http origin request headers for the backend
- `origin_url` (String) The configured backend type http for the distribution
- `region` (String) The region where the bucket is hosted. Required if type is 'bucket'.

<a id="nestedatt--config--backend--credentials"></a>
### Nested Schema for `config.backend.credentials`

Required:

- `access_key_id` (String, Sensitive) The access key for the bucket. Required if type is 'bucket'.
- `secret_access_key` (String, Sensitive) The access key for the bucket. Required if type is 'bucket'.



<a id="nestedatt--config--optimizer"></a>
Expand Down
24 changes: 24 additions & 0 deletions examples/resources/stackit_cdn_distribution/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@ resource "stackit_cdn_distribution" "example_distribution" {
}
}

resource "stackit_cdn_distribution" "example_bucket_distribution" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
config = {
backend = {
type = "bucket"
bucket_url = "https://my-test.object.storage.eu01.onstackit.cloud"
region = "eu01"

# Credentials are required for bucket backends
# It is strongly recommended to use variables for secrets
credentials = {
access_key_id = var.bucket_access_key
secret_access_key = var.bucket_secret_key
}
}
regions = ["EU", "US"]
blocked_countries = ["CN", "RU"]

optimizer = {
enabled = false
}
}
}

# Only use the import statement, if you want to import an existing cdn distribution
import {
to = stackit_cdn_distribution.import-example
Expand Down
197 changes: 196 additions & 1 deletion stackit/internal/services/cdn/cdn_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"time"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/stackitcloud/stackit-sdk-go/core/config"
Expand Down Expand Up @@ -357,6 +358,182 @@ func TestAccCDNDistributionResource(t *testing.T) {
},
})
}

func configBucketResources(bucketName, credentialsGroupName string) string {
return fmt.Sprintf(`
%s

resource "stackit_objectstorage_bucket" "bucket" {
project_id = "%s"
name = "%s"
}

resource "stackit_objectstorage_credentials_group" "group" {
project_id = "%s"
name = "%s"
}

resource "stackit_objectstorage_credential" "creds" {
project_id = "%s"
credentials_group_id = stackit_objectstorage_credentials_group.group.credentials_group_id
}

resource "stackit_cdn_distribution" "distribution" {
project_id = "%s"
config = {
backend = {
type = "bucket"
# Construct the URL dynamically using the bucket name
bucket_url = "https://${stackit_objectstorage_bucket.bucket.name}.object.storage.eu01.onstackit.cloud"
region = "eu01"

# Pass the keys via credentials block
credentials = {
access_key_id = stackit_objectstorage_credential.creds.access_key
secret_access_key = stackit_objectstorage_credential.creds.secret_access_key
}
}
regions = ["EU", "US"]
blocked_countries = ["CN", "RU"]

optimizer = {
enabled = false
}
}
}
`, testutil.CdnProviderConfig(),
testutil.ProjectId, bucketName,
testutil.ProjectId, credentialsGroupName,
testutil.ProjectId,
testutil.ProjectId,
)
}

func configBucketDatasource(bucketName, credentialsGroupName string) string {
return fmt.Sprintf(`
%s

data "stackit_cdn_distribution" "bucket_ds" {
project_id = stackit_cdn_distribution.distribution.project_id
distribution_id = stackit_cdn_distribution.distribution.distribution_id
}
`, configBucketResources(bucketName, credentialsGroupName))
}

func TestAccCDNDistributionBucketResource(t *testing.T) {
bucketName := fmt.Sprintf("tf-acc-bucket-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
credentialsGroupName := fmt.Sprintf("tf-acc-group-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
bucketNameUpdated := fmt.Sprintf("tf-acc-bucket-upd-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))
credentialsGroupNameUpdated := fmt.Sprintf("tf-acc-group-upd-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))

expectedBucketUrl := fmt.Sprintf("https://%s.object.storage.eu01.onstackit.cloud", bucketName)
expectedBucketUrlUpdated := fmt.Sprintf("https://%s.object.storage.eu01.onstackit.cloud", bucketNameUpdated)

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckCDNDistributionDestroy,
Steps: []resource.TestStep{
// Step 1: Create Resource (Real Bucket & Creds)
{
Config: configBucketResources(bucketName, credentialsGroupName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"),
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"),
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "1"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
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", "CN"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "RU"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "false"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.type", "bucket"),

// Verify the Bucket URL matches the one we constructed
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.bucket_url", expectedBucketUrl),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.region", "eu01"),

// CRITICAL: Verify that the CDN keys match the Object Storage keys
// We use AttrPair because the values are generated dynamically on the server side
resource.TestCheckResourceAttrPair(
"stackit_cdn_distribution.distribution", "config.backend.credentials.access_key_id",
"stackit_objectstorage_credential.creds", "access_key",
),
resource.TestCheckResourceAttrPair(
"stackit_cdn_distribution.distribution", "config.backend.credentials.secret_access_key",
"stackit_objectstorage_credential.creds", "secret_access_key",
),
),
},
{
ResourceName: "stackit_cdn_distribution.distribution",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources["stackit_cdn_distribution.distribution"]
if !ok {
return "", fmt.Errorf("couldn't find resource")
}
return fmt.Sprintf("%s,%s", testutil.ProjectId, r.Primary.Attributes["distribution_id"]), nil
},
ImportState: true,
ImportStateVerify: true,
// We MUST ignore credentials on import verification
// 1. API doesn't return them (security).
// 2. State has them (from resource creation).
ImportStateVerifyIgnore: []string{"config.backend.credentials"},
},
// Step 3: Data Source
{
Config: configBucketDatasource(bucketName, credentialsGroupName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.bucket_ds", "distribution_id"),
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.bucket_ds", "created_at"),
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.bucket_ds", "updated_at"),
resource.TestCheckResourceAttrPair("data.stackit_cdn_distribution.bucket_ds", "project_id", "stackit_cdn_distribution.distribution", "project_id"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "status", "ACTIVE"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "domains.#", "1"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "domains.0.type", "managed"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "domains.0.status", "ACTIVE"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.regions.#", "2"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.regions.0", "EU"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.regions.1", "US"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.blocked_countries.#", "2"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.blocked_countries.0", "CN"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.blocked_countries.1", "RU"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.optimizer.enabled", "false"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.type", "bucket"),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.bucket_url", expectedBucketUrl),

// Security Check: Secrets should NOT be in Data Source
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.credentials.access_key_id"),
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.bucket_ds", "config.backend.credentials.secret_access_key"),
),
},
// Step 4: Update Resource (Change Bucket & Creds)
{
Config: configBucketResources(bucketNameUpdated, credentialsGroupNameUpdated),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.bucket_url", expectedBucketUrlUpdated),

// Verify that keys have been updated to the new credentials
resource.TestCheckResourceAttrPair(
"stackit_cdn_distribution.distribution", "config.backend.credentials.access_key_id",
"stackit_objectstorage_credential.creds", "access_key",
),
resource.TestCheckResourceAttrPair(
"stackit_cdn_distribution.distribution", "config.backend.credentials.secret_access_key",
"stackit_objectstorage_credential.creds", "secret_access_key",
),
),
},
},
})
}
func testAccCheckCDNDistributionDestroy(s *terraform.State) error {
ctx := context.Background()
var client *cdn.APIClient
Expand Down Expand Up @@ -400,9 +577,26 @@ const (
)

func blockUntilDomainResolves(domain string) (net.IP, error) {
// Create a custom resolver that bypasses the local system DNS settings/cache
// and queries Google DNS (8.8.8.8) directly.
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
}
// Force query to Google DNS
return d.DialContext(ctx, network, "8.8.8.8:53")
},
}

// wait until it becomes ready
isReady := func() (net.IP, error) {
ips, err := net.LookupIP(domain)
// Use a context for the individual query timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

ips, err := r.LookupIP(ctx, "ip", domain)
if err != nil {
return nil, fmt.Errorf("error looking up IP for domain %s: %w", domain, err)
}
Expand All @@ -413,6 +607,7 @@ func blockUntilDomainResolves(domain string) (net.IP, error) {
}
return nil, fmt.Errorf("no IP for domain: %v", domain)
}

return retry(recordCheckAttempts, recordCheckInterval, isReady)
}

Expand Down
Loading
Loading