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
10 changes: 10 additions & 0 deletions docs/data-sources/cdn_distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Read-Only:
- `blocked_ips` (List of String) Restricts access to your content by specifying a list of blocked IPv4 addresses. This feature enhances security and privacy by preventing these addresses from accessing your distribution. Note: once a value is set, removing the attribute from your configuration will retain the last known value in state; to clear it explicitly, set it to an empty list.
- `default_cache_duration` (String) Sets the default cache duration for the distribution. The default cache duration is applied when a 'Cache-Control' header is not presented in the origin's response. We use ISO8601 duration format for cache duration (e.g. P1DT2H30M). Note: once a value is set, removing the attribute from your configuration will retain the last known value in state.
- `forward_host_header` (Boolean) Enable this allows the 'Host' header to be passed through to the origin.
- `log_sink` (Attributes) Configures a log sink to export the distribution's access logs. Only one of Loki or OTLP can be configured at a time; the sink is selected via the `type` attribute. Note: because the API never returns raw credentials, `username`, `password`, and `token` are preserved from state during Read operations. (see [below for nested schema](#nestedatt--config--log_sink))
- `monthly_limit_bytes` (Number) Sets the monthly limit of bandwidth in bytes that the pullzone is allowed to use. Note: once a value is set, removing the attribute from your configuration will retain the last known value in state.
- `optimizer` (Attributes) Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience. (see [below for nested schema](#nestedatt--config--optimizer))
- `redirects` (Attributes) A wrapper for a list of redirect rules that allows for redirect settings on a distribution (see [below for nested schema](#nestedatt--config--redirects))
Expand All @@ -74,6 +75,15 @@ Read-Only:
- `type` (String) The configured backend type. Possible values are: `http`, `bucket`.


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

Read-Only:

- `push_url` (String) The fully qualified URL where the CDN should push access logs (for example, `https://loki.example.com/loki/api/v1/push` for Loki or `https://otlp.example.com/otlp/v1/logs` for OTLP).
- `type` (String) The log sink protocol.


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

Expand Down
35 changes: 35 additions & 0 deletions docs/resources/cdn_distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" {
disabled_rule_ids = ["@builtin/crs/request/942151"]
log_only_rule_ids = ["@builtin/crs/response/954120"]
}

log_sink = {
# When using STACKIT Logs, you need to prepend the protocol part (i.e. https://) to the push_url like:
# push_url = "https://${stackit_logs_instance.logs.ingest_otlp_url}"
push_url = "https://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.logs.eu01.onstackit.cloud/otlp/v1/logs"
type = "otlp"
credentials = {
type = "bearer"
# Use the token generated e.g. by stackit_logs_access_token:
# token = stackit_logs_access_token.logs_token.access_token
token = "eyXXXXX......"
}
}
}
}
```
Expand Down Expand Up @@ -158,6 +171,7 @@ Optional:
- `blocked_ips` (List of String) Restricts access to your content by specifying a list of blocked IPv4 addresses. This feature enhances security and privacy by preventing these addresses from accessing your distribution. Note: once a value is set, removing the attribute from your configuration will retain the last known value in state; to clear it explicitly, set it to an empty list.
- `default_cache_duration` (String) Sets the default cache duration for the distribution. The default cache duration is applied when a 'Cache-Control' header is not presented in the origin's response. We use ISO8601 duration format for cache duration (e.g. P1DT2H30M). Note: once a value is set, removing the attribute from your configuration will retain the last known value in state.
- `forward_host_header` (Boolean) Enable this allows the 'Host' header to be passed through to the origin.
- `log_sink` (Attributes) Configures a log sink to export the distribution's access logs. Only one of Loki or OTLP can be configured at a time; the sink is selected via the `type` attribute. Note: because the API never returns raw credentials, `username`, `password`, and `token` are preserved from state during Read operations. (see [below for nested schema](#nestedatt--config--log_sink))
- `monthly_limit_bytes` (Number) Sets the monthly limit of bandwidth in bytes that the pullzone is allowed to use. Note: once a value is set, removing the attribute from your configuration will retain the last known value in state.
- `optimizer` (Attributes) Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience. (see [below for nested schema](#nestedatt--config--optimizer))
- `redirects` (Attributes) A wrapper for a list of redirect rules that allows for redirect settings on a distribution (see [below for nested schema](#nestedatt--config--redirects))
Expand Down Expand Up @@ -191,6 +205,27 @@ Required:



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

Required:

- `credentials` (Attributes) Authentication credentials the CDN uses when pushing logs. Loki requires `username` and `password`. OTLP requires `type` to be set to `basic` (with `username` and `password`) or `bearer` (with `token`). (see [below for nested schema](#nestedatt--config--log_sink--credentials))
- `push_url` (String) The fully qualified URL where the CDN should push access logs (for example, `https://loki.example.com/loki/api/v1/push` for Loki or `https://otlp.example.com/otlp/v1/logs` for OTLP).
- `type` (String) The log sink protocol.Possible values are: `loki`, `otlp`.

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

Optional:

- `password` (String, Sensitive) The password corresponding to `username`. Required for Loki and for OTLP when `credentials.type` is `basic`.
- `token` (String, Sensitive) The bearer token used to authenticate. Required for OTLP when `credentials.type` is `bearer`.
- `type` (String) The authentication type when using an OTLP log sink. Leave unset for Loki.Possible values are: `basic`, `bearer`.
- `username` (String, Sensitive) The username used to authenticate. Required for Loki and for OTLP when `credentials.type` is `basic`.



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

Expand Down
16 changes: 15 additions & 1 deletion examples/resources/stackit_cdn_distribution/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,19 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" {
disabled_rule_ids = ["@builtin/crs/request/942151"]
log_only_rule_ids = ["@builtin/crs/response/954120"]
}

log_sink = {
# When using STACKIT Logs, you need to prepend the protocol part (i.e. https://) to the push_url like:
# push_url = "https://${stackit_logs_instance.logs.ingest_otlp_url}"
push_url = "https://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.logs.eu01.onstackit.cloud/otlp/v1/logs"
type = "otlp"
credentials = {
type = "bearer"
# Use the token generated e.g. by stackit_logs_access_token:
# token = stackit_logs_access_token.logs_token.access_token
token = "eyXXXXX......"
}
}
}
}
}

38 changes: 31 additions & 7 deletions stackit/internal/services/cdn/cdn_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ var testConfigVarsHttp = config.Variables{
"forward_host_header": config.BoolVariable(true),
"monthly_limit_bytes": config.IntegerVariable(104857600),
"default_cache_duration": config.StringVariable("PT2H"),
"logs_display_name": config.StringVariable("TF Acc Test"),
"log_sink_type": config.StringVariable("otlp"), // Options: "loki" or "otlp"
"waf": wafConfigVariable(
"ENABLED",
"FREE",
Expand Down Expand Up @@ -220,6 +222,7 @@ func TestAccCDNDistributionHttp(t *testing.T) {
Config: testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig() + "\n" + resourceHttpBase,
ConfigVariables: testConfigVarsHttp,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("stackit_logs_instance.logs", "ingest_otlp_url"),
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"),
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"),
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"),
Expand Down Expand Up @@ -266,6 +269,8 @@ func TestAccCDNDistributionHttp(t *testing.T) {
// WAF Checks
testutil.CheckObjectAttr("stackit_cdn_distribution.distribution", "config.waf", testConfigVarsHttp["waf"]),

resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.log_sink.type", testutil.ConvertConfigVariable(testConfigVarsHttp["log_sink_type"])),

resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
),
Expand Down Expand Up @@ -307,9 +312,15 @@ func TestAccCDNDistributionHttp(t *testing.T) {

return fmt.Sprintf("%s,%s", testutil.ProjectId, distributionId), nil
},
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"domains"},
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"domains",
"config.log_sink.credentials.type",
"config.log_sink.credentials.token",
"config.log_sink.credentials.username",
"config.log_sink.credentials.password",
},
},
{
ResourceName: "stackit_cdn_custom_domain.custom_domain",
Expand Down Expand Up @@ -393,6 +404,13 @@ func TestAccCDNDistributionHttp(t *testing.T) {
testutil.CheckListAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.0.values", testConfigVarsHttp["redirect_matcher_values"]),
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.0.value_match_condition", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_matcher_condition"])),

// LogSink Data Source
// Security Check: Secrets should NOT be in Data Source
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.distribution", "config.log_sink.credentials.type"),
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.distribution", "config.log_sink.credentials.token"),
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.distribution", "config.log_sink.credentials.username"),
resource.TestCheckNoResourceAttr("data.stackit_cdn_distribution.distribution", "config.log_sink.credentials.password"),

resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", fullDomainNameHttp),
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"),
Expand Down Expand Up @@ -433,6 +451,8 @@ func TestAccCDNDistributionHttp(t *testing.T) {
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.monthly_limit_bytes", testutil.ConvertConfigVariable(configVarsHttpUpdated()["monthly_limit_bytes"])),
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.default_cache_duration", testutil.ConvertConfigVariable(configVarsHttpUpdated()["default_cache_duration"])),

resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.log_sink.type", testutil.ConvertConfigVariable(configVarsHttpUpdated()["log_sink_type"])),

// Checking WAF Mutated Configurations
testutil.CheckObjectAttr("stackit_cdn_distribution.distribution", "config.waf", configVarsHttpUpdated()["waf"]),

Expand Down Expand Up @@ -527,7 +547,8 @@ func TestAccCDNDistributionBucket(t *testing.T) {
// 1. API doesn't return them (security).
// 2. State has them (from resource creation).
ImportStateVerifyIgnore: []string{
"config.backend.credentials"},
"config.backend.credentials",
},
},
// Data Source
{
Expand Down Expand Up @@ -605,8 +626,11 @@ func testAccCheckCDNDistributionDestroy(s *terraform.State) error {
continue
}
// terraform ID: "[project_id],[distribution_id]"
distributionId := strings.Split(rs.Primary.ID, core.Separator)[1]
distributionsToDestroy = append(distributionsToDestroy, distributionId)
splitId := strings.Split(rs.Primary.ID, core.Separator)
if len(splitId) > 1 {
distributionId := strings.Split(rs.Primary.ID, core.Separator)[1]
distributionsToDestroy = append(distributionsToDestroy, distributionId)
}
}

for _, dist := range distributionsToDestroy {
Expand Down Expand Up @@ -634,7 +658,7 @@ func blockUntilDomainResolves(domain string) (net.IP, error) {
PreferGo: true,
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Millisecond * time.Duration(10000),
Timeout: time.Second * time.Duration(10),
}
// Force query to Google DNS
return d.DialContext(ctx, network, "8.8.8.8:53")
Expand Down
62 changes: 62 additions & 0 deletions stackit/internal/services/cdn/distribution/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ var dataSourceBackendTypes = map[string]attr.Type{
"region": types.StringType,
}

// dataSourcelogSinkTypes specifically leaves out the credentials
var dataSourcelogSinkTypes = map[string]attr.Type{
"type": types.StringType,
"push_url": types.StringType,
}

var dataSourceConfigTypes = map[string]attr.Type{
"backend": types.ObjectType{AttrTypes: dataSourceBackendTypes},
"regions": types.ListType{ElemType: types.StringType},
Expand All @@ -54,6 +60,9 @@ var dataSourceConfigTypes = map[string]attr.Type{
},
"strip_response_cookies": types.BoolType,
"forward_host_header": types.BoolType,
"log_sink": types.ObjectType{
AttrTypes: dataSourcelogSinkTypes, // Shared from resource.go

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like that comment is outdated?

Suggested change
AttrTypes: dataSourcelogSinkTypes, // Shared from resource.go
AttrTypes: dataSourcelogSinkTypes,

},
}

type distributionDataSource struct {
Expand Down Expand Up @@ -250,6 +259,20 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe
},
},
},
"log_sink": schema.SingleNestedAttribute{
Description: schemaDescriptions["config_log_sink"],
Computed: true,
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Computed: true,
Description: schemaDescriptions["config_log_sink_type"],
},
"push_url": schema.StringAttribute{
Computed: true,
Description: schemaDescriptions["config_log_sink_push_url"],
},
},
},
"redirects": schema.SingleNestedAttribute{
Computed: true,
Description: schemaDescriptions["config_redirects"],
Expand Down Expand Up @@ -721,6 +744,13 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution,
monthlyLimitBytes = types.Int64Null()
}

// LogSink: the data source only exposes the non-sensitive attributes
// (type, push_url). Credentials are intentionally excluded.
logSinkVal, err := mapLogSinkDataSource(distribution.Config.LogSink)
if err != nil {
return err
}

// Use dataSourceConfigTypes
cfg, diags := types.ObjectValue(dataSourceConfigTypes, map[string]attr.Value{
"backend": backend,
Expand All @@ -733,6 +763,7 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution,
"redirects": redirectsVal,
"waf": wafVal,
"tls": tlsVal,
"log_sink": logSinkVal,
"strip_response_cookies": types.BoolValue(distribution.Config.StripResponseCookies),
"forward_host_header": types.BoolValue(distribution.Config.ForwardHostHeader),
})
Expand Down Expand Up @@ -775,3 +806,34 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution,

return nil
}

// mapLogSinkDataSource maps the API log_sink response into a data-source
// object exposing only the non-sensitive attributes (type, push_url).
//
// Returns a null object when the API response has no log_sink configured.
func mapLogSinkDataSource(apiLogSink *cdnSdk.ConfigLogSink) (types.Object, error) {
if apiLogSink == nil {
return types.ObjectNull(dataSourcelogSinkTypes), nil
}

var typeVal, pushUrlVal types.String
switch {
case apiLogSink.LokiLogSink != nil:
typeVal = types.StringValue(string(apiLogSink.LokiLogSink.Type))
pushUrlVal = types.StringValue(apiLogSink.LokiLogSink.PushUrl)
case apiLogSink.OtlpLogSink != nil:
typeVal = types.StringValue(string(apiLogSink.OtlpLogSink.Type))
pushUrlVal = types.StringValue(apiLogSink.OtlpLogSink.PushUrl)
default:
return types.ObjectNull(dataSourcelogSinkTypes), nil
}

logSinkObj, diags := types.ObjectValue(dataSourcelogSinkTypes, map[string]attr.Value{
"type": typeVal,
"push_url": pushUrlVal,
})
if diags.HasError() {
return types.ObjectNull(dataSourcelogSinkTypes), core.DiagsToError(diags)
}
return logSinkObj, nil
}
Loading
Loading