Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion 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:

- `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) 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`.
- `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
30 changes: 28 additions & 2 deletions docs/resources/cdn_distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ 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-private-bucket.s3.eu-central-1.amazonaws.com"
region = "eu01"

# Credentials are required for bucket backends
# It is strongly recommended to use variables for secrets
access_key = var.bucket_access_key
secret_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 +100,17 @@ 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:

- `access_key` (String, Sensitive) The access key for the bucket. Required if type is 'bucket'.
- `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) 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
- `region` (String) The region where the bucket is hosted. Required if type is 'bucket'.
- `secret_key` (String, Sensitive) The secret key for the bucket. Required if type is 'bucket'.


<a id="nestedatt--config--optimizer"></a>
Expand Down
22 changes: 22 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,28 @@ 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-private-bucket.s3.eu-central-1.amazonaws.com"
region = "eu01"

# Credentials are required for bucket backends
# It is strongly recommended to use variables for secrets
access_key = var.bucket_access_key
secret_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
127 changes: 126 additions & 1 deletion stackit/internal/services/cdn/cdn_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,112 @@ func TestAccCDNDistributionResource(t *testing.T) {
},
})
}
func configBucketResources(bucketUrl, region, accessKey, secretKey string) string {
return fmt.Sprintf(`
%s

resource "stackit_cdn_distribution" "distribution" {
project_id = "%s"
config = {
backend = {
type = "bucket"
bucket_url = "%s"
region = "%s"
access_key = "%s"
secret_key = "%s"
}
regions = ["EU", "US"]
blocked_countries = ["CN", "RU"]

optimizer = {
enabled = false
}
}
}
`, testutil.CdnProviderConfig(), testutil.ProjectId, bucketUrl, region, accessKey, secretKey)
}
func randomString(n int) string {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
bytes := make([]byte, n)
if _, err := cryptoRand.Read(bytes); err != nil {
panic("failed to generate random string: " + err.Error())
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes)
}
func TestAccCDNDistributionBucketResource(t *testing.T) {
// Define dummy bucket data
// Note: We use dummy credentials. The CDN API might validate format,
// but usually accepts valid-looking strings for async creation.
bucketUrl := "https://my-private-bucket.s3.eu-central-1.amazonaws.com"
bucketRegion := "eu01"
accessKey := strings.ToUpper(randomString(16))
secretKey := randomString(40)

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckCDNDistributionDestroy,
Steps: []resource.TestStep{
// 1. Distribution Create with Bucket Backend
{
Config: configBucketResources(bucketUrl, bucketRegion, accessKey, secretKey),
Check: resource.ComposeAggregateTestCheckFunc(
// Basic Identity Checks
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"),
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),

// Backend Type Check
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.type", "bucket"),

// Bucket Specific Field Checks
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.bucket_url", bucketUrl),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.region", bucketRegion),

// CRITICAL: Check Credentials Persistence
// The API returns null for these, so if this passes,
// it means your logic in resource.go (mapFields) correctly
// restored the secrets from the Terraform state.
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.access_key", accessKey),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.backend.secret_key", secretKey),

// Ensure HTTP fields are not set
resource.TestCheckNoResourceAttr("stackit_cdn_distribution.distribution", "config.backend.origin_url"),
),
},
// 2. Import Test
// This verifies that importing a bucket distribution works,
// although note that imported resources usually lose secrets (access_key/secret_key)
// because they are not in the API response.
{
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 stackit_cdn_distribution.distribution")
}
distributionId, ok := r.Primary.Attributes["distribution_id"]
if !ok {
return "", fmt.Errorf("couldn't find attribute distribution_id")
}

return fmt.Sprintf("%s,%s", testutil.ProjectId, distributionId), nil
},
ImportState: true,
ImportStateVerify: true,
// We MUST ignore credentials on import verification
// because the API does not return them, so Terraform will see them as null/empty
// in the new state, differing from the config.
ImportStateVerifyIgnore: []string{
"config.backend.access_key",
"config.backend.secret_key",
},
},
},
})
}
func testAccCheckCDNDistributionDestroy(s *terraform.State) error {
ctx := context.Background()
var client *cdn.APIClient
Expand Down Expand Up @@ -376,9 +482,27 @@ 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, address 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 @@ -389,6 +513,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