diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md
index 1f046144c..099a24799 100644
--- a/docs/data-sources/cdn_distribution.md
+++ b/docs/data-sources/cdn_distribution.md
@@ -51,6 +51,7 @@ Read-Only:
- `backend` (Attributes) The configured backend for the distribution (see [below for nested schema](#nestedatt--config--backend))
- `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))
- `regions` (List of String) The configured regions where content will be hosted
@@ -74,6 +75,36 @@ Read-Only:
- `enabled` (Boolean)
+
+### Nested Schema for `config.redirects`
+
+Read-Only:
+
+- `rules` (Attributes List) A list of redirect rules. The order of rules matters for evaluation (see [below for nested schema](#nestedatt--config--redirects--rules))
+
+
+### Nested Schema for `config.redirects.rules`
+
+Read-Only:
+
+- `description` (String) An optional description for the redirect rule
+- `enabled` (Boolean) A toggle to enable or disable the redirect rule. Default to true
+- `matchers` (Attributes List) A list of matchers that define when this rule should apply. At least one matcher is required (see [below for nested schema](#nestedatt--config--redirects--rules--matchers))
+- `rule_match_condition` (String) Defines how multiple matchers within this rule are combined (ALL, ANY, NONE). Defaults to ANY.
+- `status_code` (Number) The HTTP status code for the redirect. Must be one of 301, 302, 303, 307, or 308.
+- `target_url` (String) The target URL to redirect to. Must be a valid URI
+
+
+### Nested Schema for `config.redirects.rules.matchers`
+
+Read-Only:
+
+- `value_match_condition` (String) Defines how multiple matchers within this rule are combined (ALL, ANY, NONE). Defaults to ANY.
+- `values` (List of String) A list of glob patterns to match against the request path. At least one value is required. Examples: "/shop/*" or "*/img/*"
+
+
+
+
### Nested Schema for `domains`
diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md
index baa7971ce..7a424a129 100644
--- a/docs/resources/cdn_distribution.md
+++ b/docs/resources/cdn_distribution.md
@@ -96,6 +96,7 @@ Optional:
- `blocked_countries` (List of String) The configured countries where distribution of content is blocked
- `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))
### Nested Schema for `config.backend`
@@ -131,6 +132,42 @@ Optional:
- `enabled` (Boolean)
+
+### Nested Schema for `config.redirects`
+
+Required:
+
+- `rules` (Attributes List) A list of redirect rules. The order of rules matters for evaluation (see [below for nested schema](#nestedatt--config--redirects--rules))
+
+
+### Nested Schema for `config.redirects.rules`
+
+Required:
+
+- `matchers` (Attributes List) A list of matchers that define when this rule should apply. At least one matcher is required (see [below for nested schema](#nestedatt--config--redirects--rules--matchers))
+- `status_code` (Number) The HTTP status code for the redirect. Must be one of 301, 302, 303, 307, or 308.
+- `target_url` (String) The target URL to redirect to. Must be a valid URI
+
+Optional:
+
+- `description` (String) An optional description for the redirect rule
+- `enabled` (Boolean) A toggle to enable or disable the redirect rule. Default to true
+- `rule_match_condition` (String) Defines how multiple matchers within this rule are combined (ALL, ANY, NONE). Defaults to ANY.
+
+
+### Nested Schema for `config.redirects.rules.matchers`
+
+Required:
+
+- `values` (List of String) A list of glob patterns to match against the request path. At least one value is required. Examples: "/shop/*" or "*/img/*"
+
+Optional:
+
+- `value_match_condition` (String) Defines how multiple matchers within this rule are combined (ALL, ANY, NONE). Defaults to ANY.
+
+
+
+
### Nested Schema for `domains`
diff --git a/examples/resources/stackit_cdn_distribution/resource.tf b/examples/resources/stackit_cdn_distribution/resource.tf
index 1e3d1dacd..4c37818bf 100644
--- a/examples/resources/stackit_cdn_distribution/resource.tf
+++ b/examples/resources/stackit_cdn_distribution/resource.tf
@@ -38,6 +38,24 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" {
optimizer = {
enabled = false
}
+
+ redirects = {
+ rules = [
+ {
+ description = "test redirect"
+ enabled = true
+ rule_match_condition = "ANY"
+ status_code = 302
+ target_url = "https://stackit.de/"
+ matchers = [
+ {
+ values = ["*/otherPath/"]
+ value_match_condition = "ANY"
+ }
+ ]
+ }
+ ]
+ }
}
}
diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go
index f352d863b..8ba61bc81 100644
--- a/stackit/internal/services/cdn/distribution/datasource.go
+++ b/stackit/internal/services/cdn/distribution/datasource.go
@@ -38,6 +38,9 @@ var dataSourceConfigTypes = map[string]attr.Type{
"optimizer": types.ObjectType{
AttrTypes: optimizerTypes, // Shared from resource.go
},
+ "redirects": types.ObjectType{
+ AttrTypes: redirectsTypes, // Shared from resource.go
+ },
}
type distributionDataSource struct {
@@ -199,6 +202,57 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe
},
},
},
+ "redirects": schema.SingleNestedAttribute{
+ Computed: true,
+ Description: schemaDescriptions["config_redirects"],
+ Attributes: map[string]schema.Attribute{
+ "rules": schema.ListNestedAttribute{
+ Description: schemaDescriptions["config_redirects_rules"],
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "description": schema.StringAttribute{
+ Description: schemaDescriptions["config_redirects_rule_description"],
+ Computed: true,
+ },
+ "enabled": schema.BoolAttribute{
+ Computed: true,
+ Description: schemaDescriptions["config_redirects_rule_enabled"],
+ },
+ "target_url": schema.StringAttribute{
+ Computed: true,
+ Description: schemaDescriptions["config_redirects_rule_target_url"],
+ },
+ "status_code": schema.Int32Attribute{
+ Computed: true,
+ Description: schemaDescriptions["config_redirects_rule_status_code"],
+ },
+ "rule_match_condition": schema.StringAttribute{
+ Computed: true,
+ Description: schemaDescriptions["config_redirects_rule_match_condition"],
+ },
+ "matchers": schema.ListNestedAttribute{
+ Description: schemaDescriptions["config_redirects_rule_matchers"],
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "values": schema.ListAttribute{
+ Description: schemaDescriptions["config_redirects_rule_matcher_values"],
+ Computed: true,
+ ElementType: types.StringType,
+ },
+ "value_match_condition": schema.StringAttribute{
+ Description: schemaDescriptions["config_redirects_rule_match_condition"],
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
},
},
},
@@ -300,6 +354,99 @@ func mapDataSourceFields(ctx context.Context, distribution *cdn.Distribution, mo
return core.DiagsToError(diags)
}
+ // redirects
+ redirectsVal := types.ObjectNull(redirectsTypes)
+ if distribution.Config != nil && distribution.Config.Redirects != nil && distribution.Config.Redirects.Rules != nil {
+ var tfRules []attr.Value
+ for _, r := range *distribution.Config.Redirects.Rules {
+ var tfMatchers []attr.Value
+ if r.Matchers != nil {
+ for _, m := range *r.Matchers {
+ var tfValues []attr.Value
+ if m.Values != nil {
+ for _, v := range *m.Values {
+ tfValues = append(tfValues, types.StringValue(v))
+ }
+ }
+ tfValuesList, diags := types.ListValue(types.StringType, tfValues)
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+
+ tfValMatchCond := types.StringNull()
+ if m.ValueMatchCondition != nil {
+ tfValMatchCond = types.StringValue(string(*m.ValueMatchCondition))
+ }
+
+ tfMatcherObj, diags := types.ObjectValue(matcherTypes, map[string]attr.Value{
+ "values": tfValuesList,
+ "value_match_condition": tfValMatchCond,
+ })
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+ tfMatchers = append(tfMatchers, tfMatcherObj)
+ }
+ }
+
+ tfMatchersList, diags := types.ListValue(types.ObjectType{AttrTypes: matcherTypes}, tfMatchers)
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+
+ tfDesc := types.StringNull()
+ if r.Description != nil {
+ tfDesc = types.StringValue(*r.Description)
+ }
+
+ tfEnabled := types.BoolNull()
+ if r.Enabled != nil {
+ tfEnabled = types.BoolValue(*r.Enabled)
+ }
+
+ tfTargetUrl := types.StringNull()
+ if r.TargetUrl != nil {
+ tfTargetUrl = types.StringValue(*r.TargetUrl)
+ }
+
+ tfStatusCode := types.Int32Null()
+ if r.StatusCode != nil {
+ tfStatusCode = types.Int32Value(int32(*r.StatusCode))
+ }
+
+ tfRuleMatchCond := types.StringNull()
+ if r.RuleMatchCondition != nil {
+ tfRuleMatchCond = types.StringValue(string(*r.RuleMatchCondition))
+ }
+
+ tfRuleObj, diags := types.ObjectValue(redirectRuleTypes, map[string]attr.Value{
+ "description": tfDesc,
+ "enabled": tfEnabled,
+ "target_url": tfTargetUrl,
+ "status_code": tfStatusCode,
+ "rule_match_condition": tfRuleMatchCond,
+ "matchers": tfMatchersList,
+ })
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+ tfRules = append(tfRules, tfRuleObj)
+ }
+
+ tfRulesList, diags := types.ListValue(types.ObjectType{AttrTypes: redirectRuleTypes}, tfRules)
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+
+ var objDiags diag.Diagnostics
+ redirectsVal, objDiags = types.ObjectValue(redirectsTypes, map[string]attr.Value{
+ "rules": tfRulesList,
+ })
+ if objDiags.HasError() {
+ return core.DiagsToError(objDiags)
+ }
+ }
+
// Prepare Backend Values
var backendValues map[string]attr.Value
originRequestHeaders := types.MapNull(types.StringType)
@@ -383,6 +530,7 @@ func mapDataSourceFields(ctx context.Context, distribution *cdn.Distribution, mo
"regions": modelRegions,
"blocked_countries": modelBlockedCountries,
"optimizer": optimizerVal,
+ "redirects": redirectsVal,
})
if diags.HasError() {
return core.DiagsToError(diags)
diff --git a/stackit/internal/services/cdn/distribution/datasource_test.go b/stackit/internal/services/cdn/distribution/datasource_test.go
index 5bf117032..fb62b1874 100644
--- a/stackit/internal/services/cdn/distribution/datasource_test.go
+++ b/stackit/internal/services/cdn/distribution/datasource_test.go
@@ -8,6 +8,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
)
@@ -39,13 +40,53 @@ func TestMapDataSourceFields(t *testing.T) {
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{
"enabled": types.BoolValue(true),
})
+ redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes
config := types.ObjectValueMust(dataSourceConfigTypes, map[string]attr.Value{
"backend": backend,
"regions": regionsFixture,
"blocked_countries": blockedCountriesFixture,
"optimizer": types.ObjectNull(optimizerTypes),
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
+ redirectsInput := &cdn.RedirectConfig{
+ Rules: &[]cdn.RedirectRule{
+ {
+ Description: cdn.PtrString("Test redirect"),
+ Enabled: cdn.PtrBool(true),
+ TargetUrl: cdn.PtrString("https://example.com/redirect"),
+ StatusCode: cdn.RedirectRuleStatusCode(301).Ptr(),
+ RuleMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ Matchers: &[]cdn.Matcher{
+ {
+ Values: &[]string{"/shop/*"},
+ ValueMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ },
+ },
+ },
+ },
+ }
+ matcherValuesExpected := types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("/shop/*"),
+ })
+ matcherValExpected := types.ObjectValueMust(matcherTypes, map[string]attr.Value{
+ "values": matcherValuesExpected,
+ "value_match_condition": types.StringValue("ANY"),
+ })
+ matchersListExpected := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherValExpected})
+
+ ruleValExpected := types.ObjectValueMust(redirectRuleTypes, map[string]attr.Value{
+ "description": types.StringValue("Test redirect"),
+ "enabled": types.BoolValue(true),
+ "target_url": types.StringValue("https://example.com/redirect"),
+ "status_code": types.Int32Value(301),
+ "rule_match_condition": types.StringValue("ANY"),
+ "matchers": matchersListExpected,
+ })
+ rulesListExpected := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleValExpected})
+ redirectsConfigExpected := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{
+ "rules": rulesListExpected,
+ })
emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{})
managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{
"name": types.StringValue("test.stackit-cdn.com"),
@@ -132,6 +173,7 @@ func TestMapDataSourceFields(t *testing.T) {
"regions": regionsFixture,
"optimizer": optimizer,
"blocked_countries": blockedCountriesFixture,
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Input: distributionFixture(func(d *cdn.Distribution) {
@@ -157,6 +199,7 @@ func TestMapDataSourceFields(t *testing.T) {
"regions": regionsFixture,
"blocked_countries": blockedCountriesFixture,
"optimizer": types.ObjectNull(optimizerTypes),
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
IsValid: true,
@@ -176,6 +219,7 @@ func TestMapDataSourceFields(t *testing.T) {
"regions": regionsFixture,
"optimizer": types.ObjectNull(optimizerTypes),
"blocked_countries": blockedCountriesFixture,
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Input: distributionFixture(func(d *cdn.Distribution) {
@@ -192,6 +236,21 @@ func TestMapDataSourceFields(t *testing.T) {
}),
IsValid: true,
},
+ "happy_path_with_redirects": {
+ Expected: expectedModel(func(m *Model) {
+ m.Config = types.ObjectValueMust(dataSourceConfigTypes, map[string]attr.Value{
+ "backend": backend,
+ "regions": regionsFixture,
+ "optimizer": types.ObjectNull(optimizerTypes),
+ "blocked_countries": blockedCountriesFixture,
+ "redirects": redirectsConfigExpected,
+ })
+ }),
+ Input: distributionFixture(func(d *cdn.Distribution) {
+ d.Config.Redirects = redirectsInput
+ }),
+ IsValid: true,
+ },
"happy_path_custom_domain": {
Expected: expectedModel(func(m *Model) {
managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{
diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go
index 47e36ffa4..774bf6bbb 100644
--- a/stackit/internal/services/cdn/distribution/resource.go
+++ b/stackit/internal/services/cdn/distribution/resource.go
@@ -9,6 +9,8 @@ import (
"time"
"github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
@@ -17,8 +19,10 @@ import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -43,29 +47,38 @@ var (
)
var schemaDescriptions = map[string]string{
- "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
- "distribution_id": "CDN distribution ID",
- "project_id": "STACKIT project ID associated with the distribution",
- "status": "Status of the distribution",
- "created_at": "Time when the distribution was created",
- "updated_at": "Time when the distribution was last updated",
- "errors": "List of distribution errors",
- "domains": "List of configured domains for the distribution",
- "config": "The distribution configuration",
- "config_backend": "The configured backend for the distribution",
- "config_regions": "The configured regions where content will be hosted",
- "config_backend_type": "The configured backend type. ",
- "config_optimizer": "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.",
- "config_backend_origin_url": "The configured backend type http for the distribution",
- "config_backend_origin_request_headers": "The configured type http origin request headers for the backend",
- "config_backend_geofencing": "The configured type http to configure countries where content is allowed. A map of URLs to a list of countries",
- "config_blocked_countries": "The configured countries where distribution of content is blocked",
- "domain_name": "The name of the domain",
- "domain_status": "The status of the domain",
- "domain_type": "The type of the domain. Each distribution has one domain of type \"managed\", and domains of type \"custom\" may be additionally created by the user",
- "domain_errors": "List of domain errors",
- "config_backend_bucket_url": "The URL of the bucket (e.g. https://s3.example.com). Required if type is 'bucket'.",
- "config_backend_region": "The region where the bucket is hosted. Required if type is 'bucket'.",
+ "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
+ "distribution_id": "CDN distribution ID",
+ "project_id": "STACKIT project ID associated with the distribution",
+ "status": "Status of the distribution",
+ "created_at": "Time when the distribution was created",
+ "updated_at": "Time when the distribution was last updated",
+ "errors": "List of distribution errors",
+ "domains": "List of configured domains for the distribution",
+ "config": "The distribution configuration",
+ "config_backend": "The configured backend for the distribution",
+ "config_regions": "The configured regions where content will be hosted",
+ "config_backend_type": "The configured backend type. ",
+ "config_optimizer": "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.",
+ "config_backend_origin_url": "The configured backend type http for the distribution",
+ "config_backend_origin_request_headers": "The configured type http origin request headers for the backend",
+ "config_backend_geofencing": "The configured type http to configure countries where content is allowed. A map of URLs to a list of countries",
+ "config_blocked_countries": "The configured countries where distribution of content is blocked",
+ "config_redirects": "A wrapper for a list of redirect rules that allows for redirect settings on a distribution",
+ "config_redirects_rules": "A list of redirect rules. The order of rules matters for evaluation",
+ "config_redirects_rule_description": "An optional description for the redirect rule",
+ "config_redirects_rule_enabled": "A toggle to enable or disable the redirect rule. Default to true",
+ "config_redirects_rule_target_url": "The target URL to redirect to. Must be a valid URI",
+ "config_redirects_rule_status_code": "The HTTP status code for the redirect. Must be one of 301, 302, 303, 307, or 308.",
+ "config_redirects_rule_matchers": "A list of matchers that define when this rule should apply. At least one matcher is required",
+ "config_redirects_rule_matcher_values": "A list of glob patterns to match against the request path. At least one value is required. Examples: \"/shop/*\" or \"*/img/*\"",
+ "config_redirects_rule_match_condition": "Defines how multiple matchers within this rule are combined (ALL, ANY, NONE). Defaults to ANY.",
+ "domain_name": "The name of the domain",
+ "domain_status": "The status of the domain",
+ "domain_type": "The type of the domain. Each distribution has one domain of type \"managed\", and domains of type \"custom\" may be additionally created by the user",
+ "domain_errors": "List of domain errors",
+ "config_backend_bucket_url": "The URL of the bucket (e.g. https://s3.example.com). Required if type is 'bucket'.",
+ "config_backend_region": "The region where the bucket is hosted. Required if type is 'bucket'.",
"config_backend_credentials_access_key_id": "The access key for the bucket. Required if type is 'bucket'.",
"config_backend_credentials_secret_access_key": "The secret key for the bucket. Required if type is 'bucket'.",
"config_backend_credentials": "The credentials for the bucket. Required if type is 'bucket'.",
@@ -83,11 +96,30 @@ type Model struct {
Config types.Object `tfsdk:"config"` // the configuration of the distribution
}
+type matcher struct {
+ Values []string `tfsdk:"values"`
+ ValueMatchCondition *string `tfsdk:"value_match_condition"`
+}
+
+type redirectRule struct {
+ Description *string `tfsdk:"description"`
+ Enabled *bool `tfsdk:"enabled"`
+ TargetUrl string `tfsdk:"target_url"`
+ StatusCode int32 `tfsdk:"status_code"`
+ Matchers []matcher `tfsdk:"matchers"`
+ RuleMatchCondition *string `tfsdk:"rule_match_condition"`
+}
+
+type redirectConfig struct {
+ Rules []redirectRule `tfsdk:"rules"`
+}
+
type distributionConfig struct {
- Backend backend `tfsdk:"backend"` // The backend associated with the distribution
- Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached
- BlockedCountries *[]string `tfsdk:"blocked_countries"` // The countries for which content will be blocked
- Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration
+ Backend backend `tfsdk:"backend"` // The backend associated with the distribution
+ Redirects *redirectConfig `tfsdk:"redirects"` // A wrapper for a list of redirect rules that allows for redirect settings on a distribution
+ Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached
+ BlockedCountries *[]string `tfsdk:"blocked_countries"` // The countries for which content will be blocked
+ Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration
}
type optimizerConfig struct {
@@ -95,7 +127,7 @@ type optimizerConfig struct {
}
type backend struct {
- Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" backend is supported
+ Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" and "bucket" backend is supported
OriginURL *string `tfsdk:"origin_url"` // The origin URL of the backend
OriginRequestHeaders *map[string]string `tfsdk:"origin_request_headers"` // Request headers that should be added by the CDN distribution to incoming requests
Geofencing *map[string][]*string `tfsdk:"geofencing"` // The geofencing is an object mapping multiple alternative origins to country codes.
@@ -116,6 +148,9 @@ var configTypes = map[string]attr.Type{
"optimizer": types.ObjectType{
AttrTypes: optimizerTypes,
},
+ "redirects": types.ObjectType{
+ AttrTypes: redirectsTypes,
+ },
}
var optimizerTypes = map[string]attr.Type{
@@ -126,6 +161,32 @@ var geofencingTypes = types.MapType{ElemType: types.ListType{
ElemType: types.StringType,
}}
+var matcherTypes = map[string]attr.Type{
+ "values": types.ListType{ElemType: types.StringType},
+ "value_match_condition": types.StringType,
+}
+
+var redirectRuleTypes = map[string]attr.Type{
+ "description": types.StringType,
+ "enabled": types.BoolType,
+ "target_url": types.StringType,
+ "status_code": types.Int32Type,
+ "rule_match_condition": types.StringType,
+ "matchers": types.ListType{
+ ElemType: types.ObjectType{
+ AttrTypes: matcherTypes,
+ },
+ },
+}
+
+var redirectsTypes = map[string]attr.Type{
+ "rules": types.ListType{
+ ElemType: types.ObjectType{
+ AttrTypes: redirectRuleTypes,
+ },
+ },
+}
+
var backendTypes = map[string]attr.Type{
"type": types.StringType,
"origin_url": types.StringType,
@@ -183,6 +244,8 @@ func (r *distributionResource) Metadata(_ context.Context, req resource.Metadata
func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
backendOptions := []string{"http", "bucket"}
+ matchCondition := []string{"ANY", "ALL", "NONE"}
+ statusCode := []int32{301, 302, 303, 307, 308}
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource),
Description: "CDN distribution data source schema.",
@@ -267,6 +330,74 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques
objectvalidator.AlsoRequires(path.MatchRelative().AtName("enabled")),
},
},
+ "redirects": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: schemaDescriptions["config_redirects"],
+ Attributes: map[string]schema.Attribute{
+ "rules": schema.ListNestedAttribute{
+ Description: schemaDescriptions["config_redirects_rules"],
+ Required: true,
+ Validators: []validator.List{
+ listvalidator.SizeAtLeast(1),
+ },
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "description": schema.StringAttribute{
+ Description: schemaDescriptions["config_redirects_rule_description"],
+ Optional: true,
+ },
+ "enabled": schema.BoolAttribute{
+ Optional: true,
+ Computed: true,
+ Description: schemaDescriptions["config_redirects_rule_enabled"],
+ Default: booldefault.StaticBool(true),
+ },
+ "target_url": schema.StringAttribute{
+ Required: true,
+ Description: schemaDescriptions["config_redirects_rule_target_url"],
+ },
+ "status_code": schema.Int32Attribute{
+ Required: true,
+ Description: schemaDescriptions["config_redirects_rule_status_code"],
+ Validators: []validator.Int32{int32validator.OneOf(statusCode...)},
+ },
+ "rule_match_condition": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Description: schemaDescriptions["config_redirects_rule_match_condition"],
+ Default: stringdefault.StaticString("ANY"),
+ Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(matchCondition...)},
+ },
+ "matchers": schema.ListNestedAttribute{
+ Description: schemaDescriptions["config_redirects_rule_matchers"],
+ Required: true,
+ Validators: []validator.List{
+ listvalidator.SizeAtLeast(1),
+ },
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "values": schema.ListAttribute{
+ Description: schemaDescriptions["config_redirects_rule_matcher_values"],
+ Required: true,
+ ElementType: types.StringType,
+ Validators: []validator.List{
+ listvalidator.SizeAtLeast(1),
+ },
+ },
+ "value_match_condition": schema.StringAttribute{
+ Optional: true,
+ Description: schemaDescriptions["config_redirects_rule_match_condition"],
+ Default: stringdefault.StaticString("ANY"),
+ Computed: true,
+ Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(matchCondition...)},
+ },
+ },
+ },
+ }},
+ },
+ },
+ },
+ },
"backend": schema.SingleNestedAttribute{
Required: true,
Description: schemaDescriptions["config_backend"],
@@ -567,6 +698,51 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe
blockedCountries = &tempBlockedCountries
}
+ // redirects
+ var redirectsConfig *cdn.RedirectConfig
+ if configModel.Redirects != nil {
+ sdkRules := []cdn.RedirectRule{}
+ if len(configModel.Redirects.Rules) > 0 {
+ for _, rule := range configModel.Redirects.Rules {
+ matchers := []cdn.Matcher{}
+ for _, matcher := range rule.Matchers {
+ var matchCond *cdn.MatchCondition
+ if matcher.ValueMatchCondition != nil {
+ cond := cdn.MatchCondition(*matcher.ValueMatchCondition)
+ matchCond = &cond
+ }
+
+ matchers = append(matchers, cdn.Matcher{
+ Values: &matcher.Values,
+ ValueMatchCondition: matchCond,
+ })
+ }
+
+ var ruleMatchCond *cdn.MatchCondition
+ if rule.RuleMatchCondition != nil {
+ cond := cdn.MatchCondition(*rule.RuleMatchCondition)
+ ruleMatchCond = &cond
+ }
+
+ statusCode := cdn.RedirectRuleStatusCode(rule.StatusCode)
+ targetUrl := rule.TargetUrl
+
+ sdkConfigRule := cdn.RedirectRule{
+ Description: rule.Description,
+ Enabled: rule.Enabled,
+ Matchers: &matchers,
+ RuleMatchCondition: ruleMatchCond,
+ StatusCode: &statusCode,
+ TargetUrl: &targetUrl,
+ }
+ sdkRules = append(sdkRules, sdkConfigRule)
+ }
+ }
+ redirectsConfig = &cdn.RedirectConfig{
+ Rules: &sdkRules,
+ }
+ }
+
configPatchBackend := &cdn.ConfigPatchBackend{}
if configModel.Backend.Type == "http" {
@@ -611,6 +787,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe
Backend: configPatchBackend,
Regions: ®ions,
BlockedCountries: blockedCountries,
+ Redirects: redirectsConfig,
}
if !utils.IsUndefined(configModel.Optimizer) {
@@ -773,6 +950,99 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model
}
}
+ // redirects
+ redirectsVal := types.ObjectNull(redirectsTypes)
+ if distribution.Config != nil && distribution.Config.Redirects != nil && distribution.Config.Redirects.Rules != nil {
+ var tfRules []attr.Value
+ for _, r := range *distribution.Config.Redirects.Rules {
+ var tfMatchers []attr.Value
+ if r.Matchers != nil {
+ for _, m := range *r.Matchers {
+ var tfValues []attr.Value
+ if m.Values != nil {
+ for _, v := range *m.Values {
+ tfValues = append(tfValues, types.StringValue(v))
+ }
+ }
+ tfValuesList, diags := types.ListValue(types.StringType, tfValues)
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+
+ tfValMatchCond := types.StringNull()
+ if m.ValueMatchCondition != nil {
+ tfValMatchCond = types.StringValue(string(*m.ValueMatchCondition))
+ }
+
+ tfMatcherObj, diags := types.ObjectValue(matcherTypes, map[string]attr.Value{
+ "values": tfValuesList,
+ "value_match_condition": tfValMatchCond,
+ })
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+ tfMatchers = append(tfMatchers, tfMatcherObj)
+ }
+ }
+
+ tfMatchersList, diags := types.ListValue(types.ObjectType{AttrTypes: matcherTypes}, tfMatchers)
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+
+ tfDesc := types.StringNull()
+ if r.Description != nil {
+ tfDesc = types.StringValue(*r.Description)
+ }
+
+ tfEnabled := types.BoolNull()
+ if r.Enabled != nil {
+ tfEnabled = types.BoolValue(*r.Enabled)
+ }
+
+ tfTargetUrl := types.StringNull()
+ if r.TargetUrl != nil {
+ tfTargetUrl = types.StringValue(*r.TargetUrl)
+ }
+
+ tfStatusCode := types.Int32Null()
+ if r.StatusCode != nil {
+ tfStatusCode = types.Int32Value(int32(*r.StatusCode))
+ }
+
+ tfRuleMatchCond := types.StringNull()
+ if r.RuleMatchCondition != nil {
+ tfRuleMatchCond = types.StringValue(string(*r.RuleMatchCondition))
+ }
+
+ tfRuleObj, diags := types.ObjectValue(redirectRuleTypes, map[string]attr.Value{
+ "description": tfDesc,
+ "enabled": tfEnabled,
+ "target_url": tfTargetUrl,
+ "status_code": tfStatusCode,
+ "rule_match_condition": tfRuleMatchCond,
+ "matchers": tfMatchersList,
+ })
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+ tfRules = append(tfRules, tfRuleObj)
+ }
+
+ tfRulesList, diags := types.ListValue(types.ObjectType{AttrTypes: redirectRuleTypes}, tfRules)
+ if diags.HasError() {
+ return core.DiagsToError(diags)
+ }
+
+ var objDiags diag.Diagnostics
+ redirectsVal, objDiags = types.ObjectValue(redirectsTypes, map[string]attr.Value{
+ "rules": tfRulesList,
+ })
+ if objDiags.HasError() {
+ return core.DiagsToError(objDiags)
+ }
+ }
+
// blockedCountries
var blockedCountries []attr.Value
if distribution.Config != nil && distribution.Config.BlockedCountries != nil {
@@ -910,6 +1180,7 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model
"regions": modelRegions,
"blocked_countries": modelBlockedCountries,
"optimizer": optimizerVal,
+ "redirects": redirectsVal,
})
if diags.HasError() {
return core.DiagsToError(diags)
@@ -1014,6 +1285,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution
Backend: backend,
BlockedCountries: cfg.BlockedCountries,
Optimizer: optimizer,
+ Redirects: cfg.Redirects,
}
return payload, nil
@@ -1023,6 +1295,7 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
if model == nil {
return nil, errors.New("model cannot be nil")
}
+
if model.Config.IsNull() || model.Config.IsUnknown() {
return nil, errors.New("config cannot be nil or unknown")
}
@@ -1057,6 +1330,53 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
}
}
+ // redirects
+ var redirectsConfig *cdn.RedirectConfig
+
+ if configModel.Redirects != nil {
+ sdkRules := []cdn.RedirectRule{}
+
+ if len(configModel.Redirects.Rules) > 0 {
+ for _, rule := range configModel.Redirects.Rules {
+ matchers := []cdn.Matcher{}
+ for _, matcher := range rule.Matchers {
+ var matchCond *cdn.MatchCondition
+ if matcher.ValueMatchCondition != nil {
+ cond := cdn.MatchCondition(*matcher.ValueMatchCondition)
+ matchCond = &cond
+ }
+
+ matchers = append(matchers, cdn.Matcher{
+ Values: &matcher.Values,
+ ValueMatchCondition: matchCond,
+ })
+ }
+
+ var ruleMatchCond *cdn.MatchCondition
+ if rule.RuleMatchCondition != nil {
+ cond := cdn.MatchCondition(*rule.RuleMatchCondition)
+ ruleMatchCond = &cond
+ }
+
+ statusCode := cdn.RedirectRuleStatusCode(rule.StatusCode)
+ targerUrl := rule.TargetUrl
+
+ sdkConfigRule := cdn.RedirectRule{
+ Description: rule.Description,
+ Enabled: rule.Enabled,
+ Matchers: &matchers,
+ RuleMatchCondition: ruleMatchCond,
+ StatusCode: &statusCode,
+ TargetUrl: &targerUrl,
+ }
+ sdkRules = append(sdkRules, sdkConfigRule)
+ }
+ }
+ redirectsConfig = &cdn.RedirectConfig{
+ Rules: &sdkRules,
+ }
+ }
+
// geofencing
geofencing := map[string][]string{}
if configModel.Backend.Geofencing != nil {
@@ -1080,6 +1400,7 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
Backend: &cdn.ConfigBackend{},
Regions: ®ions,
BlockedCountries: &blockedCountries,
+ Redirects: redirectsConfig,
}
if configModel.Backend.Type == "http" {
diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go
index 9c4cda8c3..fe859f0b1 100644
--- a/stackit/internal/services/cdn/distribution/resource_test.go
+++ b/stackit/internal/services/cdn/distribution/resource_test.go
@@ -8,6 +8,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
)
@@ -40,12 +41,40 @@ func TestToCreatePayload(t *testing.T) {
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{
"enabled": types.BoolValue(true),
})
+
+ redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes
+
config := types.ObjectValueMust(configTypes, map[string]attr.Value{
"backend": backend,
"regions": regionsFixture,
"blocked_countries": blockedCountriesFixture,
"optimizer": types.ObjectNull(optimizerTypes),
+ "redirects": types.ObjectNull(redirectsAttrTypes),
+ })
+
+ matcherValues := types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("/shop/*"),
+ })
+ matcherVal := types.ObjectValueMust(matcherTypes, map[string]attr.Value{
+ "values": matcherValues,
+ "value_match_condition": types.StringValue("ANY"),
+ })
+ matchersList := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherVal})
+
+ ruleVal := types.ObjectValueMust(redirectRuleTypes, map[string]attr.Value{
+ "description": types.StringValue("Test redirect"),
+ "enabled": types.BoolValue(true),
+ "target_url": types.StringValue("https://example.com/redirect"),
+ "status_code": types.Int32Value(301),
+ "rule_match_condition": types.StringValue("ANY"),
+ "matchers": matchersList,
})
+ rulesList := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleVal})
+
+ redirectsConfigVal := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{
+ "rules": rulesList,
+ })
+
modelFixture := func(mods ...func(*Model)) *Model {
model := &Model{
DistributionId: types.StringValue("test-distribution-id"),
@@ -85,6 +114,7 @@ func TestToCreatePayload(t *testing.T) {
"regions": regionsFixture,
"optimizer": optimizer,
"blocked_countries": blockedCountriesFixture,
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Expected: &cdn.CreateDistributionPayload{
@@ -102,6 +132,47 @@ func TestToCreatePayload(t *testing.T) {
},
IsValid: true,
},
+ "happy_path_with_redirects": {
+ Input: modelFixture(func(m *Model) {
+ m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "backend": backend,
+ "regions": regionsFixture,
+ "optimizer": types.ObjectNull(optimizerTypes),
+ "blocked_countries": blockedCountriesFixture,
+ "redirects": redirectsConfigVal,
+ })
+ }),
+ Expected: &cdn.CreateDistributionPayload{
+ Regions: &[]cdn.Region{"EU", "US"},
+ BlockedCountries: &[]string{"XX", "YY", "ZZ"},
+ Backend: &cdn.CreateDistributionPayloadBackend{
+ HttpBackendCreate: &cdn.HttpBackendCreate{
+ Geofencing: &map[string][]string{"https://de.mycoolapp.com": {"DE", "FR"}},
+ OriginRequestHeaders: &map[string]string{"testHeader0": "testHeaderValue0", "testHeader1": "testHeaderValue1"},
+ OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
+ Type: cdn.PtrString("http"),
+ },
+ },
+ Redirects: &cdn.RedirectConfig{
+ Rules: &[]cdn.RedirectRule{
+ {
+ Description: cdn.PtrString("Test redirect"),
+ Enabled: cdn.PtrBool(true),
+ TargetUrl: cdn.PtrString("https://example.com/redirect"),
+ StatusCode: cdn.RedirectRuleStatusCode(301).Ptr(),
+ RuleMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ Matchers: &[]cdn.Matcher{
+ {
+ Values: &[]string{"/shop/*"},
+ ValueMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ },
+ },
+ },
+ },
+ },
+ },
+ IsValid: true,
+ },
"happy_path_bucket": {
Input: modelFixture(func(m *Model) {
creds := types.ObjectValueMust(backendCredentialsTypes, map[string]attr.Value{
@@ -122,6 +193,7 @@ func TestToCreatePayload(t *testing.T) {
"regions": regionsFixture, // reusing the existing one
"blocked_countries": blockedCountriesFixture,
"optimizer": types.ObjectNull(optimizerTypes),
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Expected: &cdn.CreateDistributionPayload{
@@ -203,12 +275,40 @@ func TestConvertConfig(t *testing.T) {
blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")}
blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries)
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{"enabled": types.BoolValue(true)})
+
+ redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes
+
config := types.ObjectValueMust(configTypes, map[string]attr.Value{
"backend": backend,
"regions": regionsFixture,
"optimizer": types.ObjectNull(optimizerTypes),
"blocked_countries": blockedCountriesFixture,
+ "redirects": types.ObjectNull(redirectsAttrTypes),
+ })
+
+ matcherValues := types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("/shop/*"),
+ })
+ matcherVal := types.ObjectValueMust(matcherTypes, map[string]attr.Value{
+ "values": matcherValues,
+ "value_match_condition": types.StringValue("ANY"),
})
+ matchersList := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherVal})
+
+ ruleVal := types.ObjectValueMust(redirectRuleTypes, map[string]attr.Value{
+ "description": types.StringValue("Test redirect"),
+ "enabled": types.BoolValue(true),
+ "target_url": types.StringValue("https://example.com/redirect"),
+ "status_code": types.Int32Value(301),
+ "rule_match_condition": types.StringValue("ANY"),
+ "matchers": matchersList,
+ })
+ rulesList := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleVal})
+
+ redirectsConfigVal := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{
+ "rules": rulesList,
+ })
+
modelFixture := func(mods ...func(*Model)) *Model {
model := &Model{
DistributionId: types.StringValue("test-distribution-id"),
@@ -220,6 +320,7 @@ func TestConvertConfig(t *testing.T) {
}
return model
}
+
tests := map[string]struct {
Input *Model
Expected *cdn.Config
@@ -253,6 +354,7 @@ func TestConvertConfig(t *testing.T) {
"regions": regionsFixture,
"optimizer": optimizer,
"blocked_countries": blockedCountriesFixture,
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Expected: &cdn.Config{
@@ -275,6 +377,52 @@ func TestConvertConfig(t *testing.T) {
},
IsValid: true,
},
+ "happy_path_with_redirects": {
+ Input: modelFixture(func(m *Model) {
+ m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "backend": backend,
+ "regions": regionsFixture,
+ "optimizer": types.ObjectNull(optimizerTypes),
+ "blocked_countries": blockedCountriesFixture,
+ "redirects": redirectsConfigVal, // Injetando o mock aqui
+ })
+ }),
+ Expected: &cdn.Config{
+ Backend: &cdn.ConfigBackend{
+ HttpBackend: &cdn.HttpBackend{
+ OriginRequestHeaders: &map[string]string{
+ "testHeader0": "testHeaderValue0",
+ "testHeader1": "testHeaderValue1",
+ },
+ OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
+ Type: cdn.PtrString("http"),
+ Geofencing: &map[string][]string{
+ "https://de.mycoolapp.com": {"DE", "FR"},
+ },
+ },
+ },
+ Regions: &[]cdn.Region{"EU", "US"},
+ BlockedCountries: &[]string{"XX", "YY", "ZZ"},
+ Redirects: &cdn.RedirectConfig{
+ Rules: &[]cdn.RedirectRule{
+ {
+ Description: cdn.PtrString("Test redirect"),
+ Enabled: cdn.PtrBool(true),
+ TargetUrl: cdn.PtrString("https://example.com/redirect"),
+ StatusCode: cdn.RedirectRuleStatusCode(301).Ptr(),
+ RuleMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ Matchers: &[]cdn.Matcher{
+ {
+ Values: &[]string{"/shop/*"},
+ ValueMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ },
+ },
+ },
+ },
+ },
+ },
+ IsValid: true,
+ },
"happy_path_bucket": {
Input: modelFixture(func(m *Model) {
creds := types.ObjectValueMust(backendCredentialsTypes, map[string]attr.Value{
@@ -295,6 +443,7 @@ func TestConvertConfig(t *testing.T) {
"regions": regionsFixture,
"blocked_countries": blockedCountriesFixture,
"optimizer": types.ObjectNull(optimizerTypes),
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Expected: &cdn.Config{
@@ -303,8 +452,6 @@ func TestConvertConfig(t *testing.T) {
Type: cdn.PtrString("bucket"),
BucketUrl: cdn.PtrString("https://s3.example.com"),
Region: cdn.PtrString("eu01"),
- // Note: config does not return credentials
-
},
},
Regions: &[]cdn.Region{"EU", "US"},
@@ -325,6 +472,7 @@ func TestConvertConfig(t *testing.T) {
IsValid: false,
},
}
+
for tn, tc := range tests {
t.Run(tn, func(t *testing.T) {
res, err := convertConfig(context.Background(), tc.Input)
@@ -373,11 +521,56 @@ func TestMapFields(t *testing.T) {
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{
"enabled": types.BoolValue(true),
})
+
+ redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes
+
config := types.ObjectValueMust(configTypes, map[string]attr.Value{
"backend": backend,
"regions": regionsFixture,
"blocked_countries": blockedCountriesFixture,
"optimizer": types.ObjectNull(optimizerTypes),
+ "redirects": types.ObjectNull(redirectsAttrTypes),
+ })
+
+ redirectsInput := &cdn.RedirectConfig{
+ Rules: &[]cdn.RedirectRule{
+ {
+ Description: cdn.PtrString("Test redirect"),
+ Enabled: cdn.PtrBool(true),
+ TargetUrl: cdn.PtrString("https://example.com/redirect"),
+ StatusCode: cdn.RedirectRuleStatusCode(301).Ptr(),
+ RuleMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ Matchers: &[]cdn.Matcher{
+ {
+ Values: &[]string{"/shop/*"},
+ ValueMatchCondition: cdn.MatchCondition("ANY").Ptr(),
+ },
+ },
+ },
+ },
+ }
+
+ matcherValuesExpected := types.ListValueMust(types.StringType, []attr.Value{
+ types.StringValue("/shop/*"),
+ })
+ matcherValExpected := types.ObjectValueMust(matcherTypes, map[string]attr.Value{
+ "values": matcherValuesExpected,
+ "value_match_condition": types.StringValue("ANY"),
+ })
+ matchersListExpected := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherValExpected})
+
+ ruleValExpected := types.ObjectValueMust(redirectRuleTypes, map[string]attr.Value{
+ "description": types.StringValue("Test redirect"),
+ "enabled": types.BoolValue(true),
+ "target_url": types.StringValue("https://example.com/redirect"),
+ "status_code": types.Int32Value(301),
+ "rule_match_condition": types.StringValue("ANY"),
+ "matchers": matchersListExpected,
+ })
+ rulesListExpected := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleValExpected})
+
+ redirectsConfigExpected := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{
+ "rules": rulesListExpected,
})
emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{})
@@ -459,6 +652,7 @@ func TestMapFields(t *testing.T) {
"regions": regionsFixture,
"blocked_countries": blockedCountriesFixture,
"optimizer": types.ObjectNull(optimizerTypes),
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
tests := map[string]struct {
Input *cdn.Distribution
@@ -478,6 +672,7 @@ func TestMapFields(t *testing.T) {
"regions": regionsFixture,
"optimizer": optimizer,
"blocked_countries": blockedCountriesFixture,
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Input: distributionFixture(func(d *cdn.Distribution) {
@@ -503,6 +698,7 @@ func TestMapFields(t *testing.T) {
"regions": regionsFixture,
"optimizer": types.ObjectNull(optimizerTypes),
"blocked_countries": blockedCountriesFixture,
+ "redirects": types.ObjectNull(redirectsAttrTypes),
})
}),
Input: distributionFixture(func(d *cdn.Distribution) {
@@ -510,6 +706,21 @@ func TestMapFields(t *testing.T) {
}),
IsValid: true,
},
+ "happy_path_with_redirects": {
+ Expected: expectedModel(func(m *Model) {
+ m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
+ "backend": backend,
+ "regions": regionsFixture,
+ "optimizer": types.ObjectNull(optimizerTypes),
+ "blocked_countries": blockedCountriesFixture,
+ "redirects": redirectsConfigExpected,
+ })
+ }),
+ Input: distributionFixture(func(d *cdn.Distribution) {
+ d.Config.Redirects = redirectsInput
+ }),
+ IsValid: true,
+ },
"happy_path_status_error": {
Expected: expectedModel(func(m *Model) {
m.Status = types.StringValue("ERROR")