From 7c989aa922761d9c13b310193de58402dd65232b Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 26 Mar 2026 17:49:55 +0100 Subject: [PATCH 01/62] feat: add redirects in convertConfig --- .../services/cdn/distribution/resource.go | 235 +++++++++++++++--- .../cdn/distribution/resource_test.go | 81 +++++- 2 files changed, 286 insertions(+), 30 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 47e36ffa4..56fe3699f 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,73 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques objectvalidator.AlsoRequires(path.MatchRelative().AtName("enabled")), }, }, + "redirects": schema.SingleNestedAttribute{ + Required: 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), + }, + "targetUrl": schema.StringAttribute{ + Required: true, + Description: schemaDescriptions["config_redirects_rule_target_url"], + }, + "statusCode": schema.Int32Attribute{ + Required: true, + Description: schemaDescriptions["config_redirects_rule_status_code"], + Validators: []validator.Int32{int32validator.OneOf(statusCode...)}, + }, + "ruleMatchCondition": 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, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + "ruleMatchCondition": 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"], @@ -1023,6 +1153,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 +1188,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 +1258,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..f2cf79ef1 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" ) @@ -203,12 +204,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 +249,7 @@ func TestConvertConfig(t *testing.T) { } return model } + tests := map[string]struct { Input *Model Expected *cdn.Config @@ -253,6 +283,7 @@ func TestConvertConfig(t *testing.T) { "regions": regionsFixture, "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, + "redirects": types.ObjectNull(redirectsAttrTypes), }) }), Expected: &cdn.Config{ @@ -275,6 +306,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 +372,7 @@ func TestConvertConfig(t *testing.T) { "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), + "redirects": types.ObjectNull(redirectsAttrTypes), }) }), Expected: &cdn.Config{ @@ -303,8 +381,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 +401,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) From 7cca33f18d2ad400b83a099e7a056e4fa50e58ca Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 26 Mar 2026 18:14:25 +0100 Subject: [PATCH 02/62] feat: add redirect in toCreatePayload --- .../services/cdn/distribution/resource.go | 1 + .../cdn/distribution/resource_test.go | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 56fe3699f..45bff3f43 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1144,6 +1144,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution Backend: backend, BlockedCountries: cfg.BlockedCountries, Optimizer: optimizer, + Redirects: cfg.Redirects, } return payload, nil diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index f2cf79ef1..2af2ef55f 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -41,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"), @@ -86,6 +114,7 @@ func TestToCreatePayload(t *testing.T) { "regions": regionsFixture, "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, + "redirects": types.ObjectNull(redirectsAttrTypes), }) }), Expected: &cdn.CreateDistributionPayload{ @@ -103,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{ @@ -123,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{ From e850a14c1e8afaa35086f272d76873161ae09f47 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 26 Mar 2026 18:21:13 +0100 Subject: [PATCH 03/62] feat: add redirect in the update function --- .../services/cdn/distribution/resource.go | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 45bff3f43..bb5133197 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -697,6 +697,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" { From cf6f25511b137609fc001ebbeae4b0489fd1f7de Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 26 Mar 2026 18:34:05 +0100 Subject: [PATCH 04/62] feat: add in the map field mapfield --- .../services/cdn/distribution/resource.go | 95 +++++++++++++++++++ .../cdn/distribution/resource_test.go | 63 ++++++++++++ 2 files changed, 158 insertions(+) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index bb5133197..bd134a414 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -786,6 +786,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Backend: configPatchBackend, Regions: ®ions, BlockedCountries: blockedCountries, + Redirects: redirectsConfig, } if !utils.IsUndefined(configModel.Optimizer) { @@ -948,6 +949,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 { @@ -1085,6 +1179,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) diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index 2af2ef55f..fe859f0b1 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -521,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{}) @@ -607,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 @@ -626,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) { @@ -651,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) { @@ -658,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") From 72d079cc3411ea0a9a2ebcefe1b5ebd68f53f9aa Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 27 Mar 2026 08:46:11 +0100 Subject: [PATCH 05/62] small fix and add datasource --- .../services/cdn/distribution/datasource.go | 148 ++++++++++++++++++ .../cdn/distribution/datasource_test.go | 59 +++++++ .../services/cdn/distribution/resource.go | 11 +- 3 files changed, 213 insertions(+), 5 deletions(-) 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 bd134a414..774bf6bbb 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -331,7 +331,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, }, "redirects": schema.SingleNestedAttribute{ - Required: true, + Optional: true, Description: schemaDescriptions["config_redirects"], Attributes: map[string]schema.Attribute{ "rules": schema.ListNestedAttribute{ @@ -352,16 +352,16 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Description: schemaDescriptions["config_redirects_rule_enabled"], Default: booldefault.StaticBool(true), }, - "targetUrl": schema.StringAttribute{ + "target_url": schema.StringAttribute{ Required: true, Description: schemaDescriptions["config_redirects_rule_target_url"], }, - "statusCode": schema.Int32Attribute{ + "status_code": schema.Int32Attribute{ Required: true, Description: schemaDescriptions["config_redirects_rule_status_code"], Validators: []validator.Int32{int32validator.OneOf(statusCode...)}, }, - "ruleMatchCondition": schema.StringAttribute{ + "rule_match_condition": schema.StringAttribute{ Optional: true, Computed: true, Description: schemaDescriptions["config_redirects_rule_match_condition"], @@ -379,11 +379,12 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques "values": schema.ListAttribute{ Description: schemaDescriptions["config_redirects_rule_matcher_values"], Required: true, + ElementType: types.StringType, Validators: []validator.List{ listvalidator.SizeAtLeast(1), }, }, - "ruleMatchCondition": schema.StringAttribute{ + "value_match_condition": schema.StringAttribute{ Optional: true, Description: schemaDescriptions["config_redirects_rule_match_condition"], Default: stringdefault.StaticString("ANY"), From cafad57d46454b6b38160bf7a957e25c06e3a23b Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 27 Mar 2026 14:26:59 +0100 Subject: [PATCH 06/62] chore: update doc --- docs/data-sources/cdn_distribution.md | 31 ++++++++++++++++++++++ docs/resources/cdn_distribution.md | 37 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) 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` From f2bcc1b1029fcb15346c8fed66b8db42988e4c0a Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 27 Mar 2026 16:01:32 +0100 Subject: [PATCH 07/62] chore: add resource example --- .../stackit_cdn_distribution/resource.tf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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" + } + ] + } + ] + } } } From 9addaea7a7ee08861bf6e327bad0836c11657b00 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 31 Mar 2026 19:02:47 +0200 Subject: [PATCH 08/62] chore: add redirect in acc test and address linter issues --- stackit/internal/services/cdn/cdn_acc_test.go | 17 ++++++++++++++++- .../services/cdn/distribution/datasource.go | 2 +- .../cdn/distribution/datasource_test.go | 7 ++++++- .../services/cdn/distribution/resource.go | 10 ++++++---- .../services/cdn/distribution/resource_test.go | 18 +++++++++++++++--- .../cdn/testdata/resource-http-base.tf | 16 ++++++++++++++++ 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 42c2387f7..d14bbff21 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -92,12 +92,15 @@ var testConfigVarsHttp = config.Variables{ "origin_request_headers_value": config.StringVariable("x-custom-value"), "certificate": config.StringVariable(string(cert)), "private_key": config.StringVariable(string(key)), + "redirect_target_url": config.StringVariable("https://example.com"), + "redirect_status_code": config.IntegerVariable(301), + "redirect_matcher_value": config.StringVariable("/shop/*"), } func configVarsHttpUpdated() config.Variables { updatedConfig := maps.Clone(testConfigVarsHttp) updatedConfig["regions"] = config.ListVariable(config.StringVariable("EU"), config.StringVariable("US"), config.StringVariable("ASIA")) - + updatedConfig["redirect_target_url"] = config.StringVariable("https://example.com/updated") return updatedConfig } @@ -157,6 +160,11 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"), 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.redirects.rules.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.target_url", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_target_url"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.status_code", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_status_code"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.0.values.0", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_matcher_value"])), 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"), @@ -277,6 +285,9 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.optimizer.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["optimizer"])), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.target_url", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_target_url"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.status_code", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_status_code"])), resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", fullDomainNameHttp), @@ -319,6 +330,10 @@ func TestAccCDNDistributionHttp(t *testing.T) { "DE", ), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.target_url", "https://example.com/updated"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.status_code", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_status_code"])), + resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainNameHttp), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"), diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index 8ba61bc81..1eb289ff9 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -411,7 +411,7 @@ func mapDataSourceFields(ctx context.Context, distribution *cdn.Distribution, mo tfStatusCode := types.Int32Null() if r.StatusCode != nil { - tfStatusCode = types.Int32Value(int32(*r.StatusCode)) + tfStatusCode = types.Int32Value(int32(*r.StatusCode)) // nolint:gosec // HTTP status codes are safely within int32 bounds } tfRuleMatchCond := types.StringNull() diff --git a/stackit/internal/services/cdn/distribution/datasource_test.go b/stackit/internal/services/cdn/distribution/datasource_test.go index 0c2c0fe76..e675a86e4 100644 --- a/stackit/internal/services/cdn/distribution/datasource_test.go +++ b/stackit/internal/services/cdn/distribution/datasource_test.go @@ -40,7 +40,12 @@ func TestMapDataSourceFields(t *testing.T) { optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(true), }) - redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes + // Safely assert the type + redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) + if !ok { + t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") + } + redirectsAttrTypes := redirectsObjType.AttrTypes config := types.ObjectValueMust(dataSourceConfigTypes, map[string]attr.Value{ "backend": backend, "regions": regionsFixture, diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 6998b4735..7ac5aca39 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -346,6 +346,8 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques "description": schema.StringAttribute{ Description: schemaDescriptions["config_redirects_rule_description"], Optional: true, + Computed: true, + Default: stringdefault.StaticString(""), }, "enabled": schema.BoolAttribute{ Optional: true, @@ -971,7 +973,7 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model return core.DiagsToError(diags) } - tfValMatchCond := types.StringNull() + tfValMatchCond := types.StringValue("ANY") if m.ValueMatchCondition != nil { tfValMatchCond = types.StringValue(string(*m.ValueMatchCondition)) } @@ -992,12 +994,12 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model return core.DiagsToError(diags) } - tfDesc := types.StringNull() + tfDesc := types.StringValue("") if r.Description != nil { tfDesc = types.StringValue(*r.Description) } - tfEnabled := types.BoolNull() + tfEnabled := types.BoolValue(true) if r.Enabled != nil { tfEnabled = types.BoolValue(*r.Enabled) } @@ -1012,7 +1014,7 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model tfStatusCode = types.Int32Value(int32(*r.StatusCode)) } - tfRuleMatchCond := types.StringNull() + tfRuleMatchCond := types.StringValue("ANY") if r.RuleMatchCondition != nil { tfRuleMatchCond = types.StringValue(string(*r.RuleMatchCondition)) } diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index 143f863e5..1bae05a7c 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -42,7 +42,11 @@ func TestToCreatePayload(t *testing.T) { "enabled": types.BoolValue(true), }) - redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes + redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) + if !ok { + t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") + } + redirectsAttrTypes := redirectsObjType.AttrTypes config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, @@ -276,7 +280,11 @@ func TestConvertConfig(t *testing.T) { blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{"enabled": types.BoolValue(true)}) - redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes + redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) + if !ok { + t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") + } + redirectsAttrTypes := redirectsObjType.AttrTypes config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, @@ -524,7 +532,11 @@ func TestMapFields(t *testing.T) { "enabled": types.BoolValue(true), }) - redirectsAttrTypes := configTypes["redirects"].(basetypes.ObjectType).AttrTypes + redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) + if !ok { + t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") + } + redirectsAttrTypes := redirectsObjType.AttrTypes config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, diff --git a/stackit/internal/services/cdn/testdata/resource-http-base.tf b/stackit/internal/services/cdn/testdata/resource-http-base.tf index 3275fd0a5..026d1b990 100644 --- a/stackit/internal/services/cdn/testdata/resource-http-base.tf +++ b/stackit/internal/services/cdn/testdata/resource-http-base.tf @@ -9,6 +9,9 @@ variable "origin_request_headers_name" {} variable "origin_request_headers_value" {} variable "certificate" {} variable "private_key" {} +variable "redirect_target_url" {} +variable "redirect_status_code" {} +variable "redirect_matcher_value" {} # dns variable "dns_zone_name" {} @@ -39,6 +42,19 @@ resource "stackit_cdn_distribution" "distribution" { optimizer = { enabled = var.optimizer } + redirects = { + rules = [ + { + target_url = var.redirect_target_url + status_code = var.redirect_status_code + matchers = [ + { + values = [var.redirect_matcher_value] + } + ] + } + ] + } backend = { type = var.backend_http_type origin_url = var.backend_origin_url From 618c3a2922756d46178d8b5922c799637f1dbc8f Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 31 Mar 2026 19:03:00 +0200 Subject: [PATCH 09/62] chore: address linter issue --- stackit/internal/services/cdn/distribution/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 7ac5aca39..64b101aa4 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1011,7 +1011,7 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model tfStatusCode := types.Int32Null() if r.StatusCode != nil { - tfStatusCode = types.Int32Value(int32(*r.StatusCode)) + tfStatusCode = types.Int32Value(int32(*r.StatusCode)) // nolint:gosec // HTTP status codes are safely within int32 bounds } tfRuleMatchCond := types.StringValue("ANY") From 9a22cefaf46af67df009081a37c816efc726e147 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 31 Mar 2026 19:05:09 +0200 Subject: [PATCH 10/62] chore: add doc --- docs/resources/cdn_distribution.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index 7a424a129..f60345443 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -56,6 +56,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" + } + ] + } + ] + } } } From cf1296789cfb3eb85f19e081f6b8019403e71dcc Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 1 Apr 2026 20:47:33 +0200 Subject: [PATCH 11/62] chore: adjust for the new sdk version --- .../services/cdn/distribution/datasource.go | 16 ++--- .../services/cdn/distribution/resource.go | 65 +++++++++---------- 2 files changed, 37 insertions(+), 44 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index da4eefa5e..ec1bba135 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -356,15 +356,15 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, // redirects redirectsVal := types.ObjectNull(redirectsTypes) - if distribution.Config != nil && distribution.Config.Redirects != nil && distribution.Config.Redirects.Rules != nil { + if distribution.Config.Redirects != nil && distribution.Config.Redirects.Rules != nil { var tfRules []attr.Value - for _, r := range *distribution.Config.Redirects.Rules { + for _, r := range distribution.Config.Redirects.Rules { var tfMatchers []attr.Value if r.Matchers != nil { - for _, m := range *r.Matchers { + for _, m := range r.Matchers { var tfValues []attr.Value if m.Values != nil { - for _, v := range *m.Values { + for _, v := range m.Values { tfValues = append(tfValues, types.StringValue(v)) } } @@ -405,13 +405,13 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, } tfTargetUrl := types.StringNull() - if r.TargetUrl != nil { - tfTargetUrl = types.StringValue(*r.TargetUrl) + if r.TargetUrl != "" { + tfTargetUrl = types.StringValue(r.TargetUrl) } tfStatusCode := types.Int32Null() - if r.StatusCode != nil { - tfStatusCode = types.Int32Value(int32(*r.StatusCode)) // nolint:gosec // HTTP status codes are safely within int32 bounds + if r.StatusCode != 0 { + tfStatusCode = types.Int32Value(int32(r.StatusCode)) // nolint:gosec // HTTP status codes are safely within int32 bounds } tfRuleMatchCond := types.StringNull() diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 71e87bf12..09982cb34 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -30,7 +30,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" cdnSdk "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" @@ -684,7 +683,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe // blockedCountries // Use a pointer to a slice to distinguish between an empty list (unblock all) and nil (no change). - var blockedCountries *[]string + var blockedCountries []string if configModel.BlockedCountries != nil { // Use a temporary slice tempBlockedCountries := []string{} @@ -699,7 +698,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } // Point to the populated slice - blockedCountries = &tempBlockedCountries + blockedCountries = tempBlockedCountries } // redirects @@ -717,7 +716,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } matchers = append(matchers, cdnSdk.Matcher{ - Values: &matcher.Values, + Values: matcher.Values, ValueMatchCondition: matchCond, }) } @@ -727,23 +726,21 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe cond := cdnSdk.MatchCondition(*rule.RuleMatchCondition) ruleMatchCond = &cond } - - statusCode := cdnSdk.RedirectRuleStatusCode(rule.StatusCode) targetUrl := rule.TargetUrl sdkConfigRule := cdnSdk.RedirectRule{ Description: rule.Description, Enabled: rule.Enabled, - Matchers: &matchers, + Matchers: matchers, RuleMatchCondition: ruleMatchCond, - StatusCode: &statusCode, - TargetUrl: &targetUrl, + StatusCode: rule.StatusCode, + TargetUrl: targetUrl, } sdkRules = append(sdkRules, sdkConfigRule) } } redirectsConfig = &cdnSdk.RedirectConfig{ - Rules: &sdkRules, + Rules: sdkRules, } } @@ -790,7 +787,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe configPatch := &cdnSdk.ConfigPatch{ Backend: configPatchBackend, - Regions: ®ions, + Regions: regions, BlockedCountries: blockedCountries, Redirects: redirectsConfig, } @@ -949,15 +946,15 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo // redirects redirectsVal := types.ObjectNull(redirectsTypes) - if distribution.Config != nil && distribution.Config.Redirects != nil && distribution.Config.Redirects.Rules != nil { + if distribution.Config.Redirects != nil && distribution.Config.Redirects.Rules != nil { var tfRules []attr.Value - for _, r := range *distribution.Config.Redirects.Rules { + for _, r := range distribution.Config.Redirects.Rules { var tfMatchers []attr.Value if r.Matchers != nil { - for _, m := range *r.Matchers { + for _, m := range r.Matchers { var tfValues []attr.Value if m.Values != nil { - for _, v := range *m.Values { + for _, v := range m.Values { tfValues = append(tfValues, types.StringValue(v)) } } @@ -998,13 +995,13 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo } tfTargetUrl := types.StringNull() - if r.TargetUrl != nil { - tfTargetUrl = types.StringValue(*r.TargetUrl) + if r.TargetUrl != "" { + tfTargetUrl = types.StringValue(r.TargetUrl) } tfStatusCode := types.Int32Null() - if r.StatusCode != nil { - tfStatusCode = types.Int32Value(int32(*r.StatusCode)) // nolint:gosec // HTTP status codes are safely within int32 bounds + if r.StatusCode > 0 { + tfStatusCode = types.Int32Value(int32(r.StatusCode)) // nolint:gosec // HTTP status codes are safely within int32 bounds } tfRuleMatchCond := types.StringValue("ANY") @@ -1275,9 +1272,8 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut }, } } - intendId := uuid.NewString() payload := &cdnSdk.CreateDistributionPayload{ - IntentId: new(intendId), + IntentId: new(uuid.NewString()), Regions: cfg.Regions, Backend: *backend, BlockedCountries: cfg.BlockedCountries, @@ -1335,42 +1331,39 @@ func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { if len(configModel.Redirects.Rules) > 0 { for _, rule := range configModel.Redirects.Rules { - matchers := []cdn.Matcher{} + matchers := []cdnSdk.Matcher{} for _, matcher := range rule.Matchers { - var matchCond *cdn.MatchCondition + var matchCond *cdnSdk.MatchCondition if matcher.ValueMatchCondition != nil { - cond := cdn.MatchCondition(*matcher.ValueMatchCondition) + cond := cdnSdk.MatchCondition(*matcher.ValueMatchCondition) matchCond = &cond } - matchers = append(matchers, cdn.Matcher{ - Values: &matcher.Values, + matchers = append(matchers, cdnSdk.Matcher{ + Values: matcher.Values, ValueMatchCondition: matchCond, }) } - var ruleMatchCond *cdn.MatchCondition + var ruleMatchCond *cdnSdk.MatchCondition if rule.RuleMatchCondition != nil { - cond := cdn.MatchCondition(*rule.RuleMatchCondition) + cond := cdnSdk.MatchCondition(*rule.RuleMatchCondition) ruleMatchCond = &cond } - statusCode := cdn.RedirectRuleStatusCode(rule.StatusCode) - targerUrl := rule.TargetUrl - - sdkConfigRule := cdn.RedirectRule{ + sdkConfigRule := cdnSdk.RedirectRule{ Description: rule.Description, Enabled: rule.Enabled, - Matchers: &matchers, + Matchers: matchers, RuleMatchCondition: ruleMatchCond, - StatusCode: &statusCode, - TargetUrl: &targerUrl, + StatusCode: rule.StatusCode, + TargetUrl: rule.TargetUrl, } sdkRules = append(sdkRules, sdkConfigRule) } } redirectsConfig = &cdnSdk.RedirectConfig{ - Rules: &sdkRules, + Rules: sdkRules, } } From 45905c6e208a19a4b6873cde0d37d6fe5a005f06 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 1 Apr 2026 20:58:11 +0200 Subject: [PATCH 12/62] chore: implement new sdk in cdn_acc_test --- stackit/internal/services/cdn/cdn_acc_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 26eaf701b..293ef0962 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -21,8 +21,8 @@ import ( "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/services/cdn" - "github.com/stackitcloud/stackit-sdk-go/services/cdn/wait" + cdnSdk "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -478,7 +478,7 @@ func TestAccCDNDistributionBucket(t *testing.T) { func testAccCheckCDNDistributionDestroy(s *terraform.State) error { ctx := context.Background() - client, err := cdn.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.CdnCustomEndpoint, false)...) + client, err := cdnSdk.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.CdnCustomEndpoint, false)...) if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -493,11 +493,11 @@ func testAccCheckCDNDistributionDestroy(s *terraform.State) error { } for _, dist := range distributionsToDestroy { - _, err := client.DeleteDistribution(ctx, testutil.ProjectId, dist).Execute() + _, err := client.DefaultAPI.DeleteDistribution(ctx, testutil.ProjectId, dist).Execute() if err != nil { return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: %w", dist, err) } - _, err = wait.DeleteDistributionWaitHandler(ctx, client, testutil.ProjectId, dist).WaitWithContext(ctx) + _, err = wait.DeleteDistributionWaitHandler(ctx, client.DefaultAPI, testutil.ProjectId, dist).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: waiting for deletion %w", dist, err) } From 23d6bd923710e71c21e7de0c23e04ee01a21b78b Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Sun, 12 Apr 2026 19:12:12 +0200 Subject: [PATCH 13/62] feat: add waf in resource --- .../services/cdn/distribution/resource.go | 304 ++++++++++++++++++ .../cdn/distribution/resource_test.go | 246 +++++++++++++- 2 files changed, 549 insertions(+), 1 deletion(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 09982cb34..2f90e8c88 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -6,6 +6,7 @@ import ( "fmt" "maps" "net/http" + "sort" "strings" "time" @@ -83,6 +84,22 @@ var schemaDescriptions = map[string]string{ "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'.", + "config_waf": "Configuration of the WAF of a distribution.", + "waf_mode": "The WAF mode. ENABLED actively blocks, LOG_ONLY logs matches but never blocks, DISABLED completely turns off inspection.", + "waf_type": "Enable or disable the Premium WAF. FREE or PREMIUM.", + "waf_paranoia_level": "Defines how aggressively the WAF should action on requests (L1 to L4).", + "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted.", + "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies.", + "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts.", + "waf_enabled_rule_ids": "Ids of the WAF rules explicitly enabled.", + "waf_disabled_rule_ids": "Ids of WAF Rules explicitly disabled.", + "waf_log_only_rule_ids": "Ids of WAF Rules explicitly marked as Log Only.", + "waf_enabled_rule_group_ids": "Ids of WAF Rule Groups explicitly enabled.", + "waf_disabled_rule_group_ids": "Ids of WAF Rule Groups explicitly disabled.", + "waf_log_only_rule_group_ids": "Ids of WAF Rule Groups explicitly marked as log Only.", + "waf_enabled_rule_collection_ids": "Ids of WAF Collections explicitly enabled.", + "waf_disabled_rule_collection_ids": "Ids of WAF Collections explicitly disabled.", + "waf_log_only_rule_collection_ids": "Ids of WAF Collections explicitly marked as log Only.", } type Model struct { @@ -121,6 +138,7 @@ type distributionConfig struct { 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 + Waf types.Object `tfsdk:"waf"` // The WAF configuration } type optimizerConfig struct { @@ -137,6 +155,24 @@ type backend struct { Credentials *backendCredentials `tfsdk:"credentials"` } +type wafConfig struct { + Mode types.String `tfsdk:"mode"` + Type types.String `tfsdk:"type"` + ParanoiaLevel types.String `tfsdk:"paranoia_level"` + AllowedHttpVersions types.List `tfsdk:"allowed_http_versions"` + AllowedRequestContentTypes types.List `tfsdk:"allowed_request_content_types"` + AllowedHttpMethods types.List `tfsdk:"allowed_http_methods"` + EnabledRuleIds types.List `tfsdk:"enabled_rule_ids"` + DisabledRuleIds types.List `tfsdk:"disabled_rule_ids"` + LogOnlyRuleIds types.List `tfsdk:"log_only_rule_ids"` + EnabledRuleGroupIds types.List `tfsdk:"enabled_rule_group_ids"` + DisabledRuleGroupIds types.List `tfsdk:"disabled_rule_group_ids"` + LogOnlyRuleGroupIds types.List `tfsdk:"log_only_rule_group_ids"` + EnabledRuleCollectionIds types.List `tfsdk:"enabled_rule_collection_ids"` + DisabledRuleCollectionIds types.List `tfsdk:"disabled_rule_collection_ids"` + LogOnlyRuleCollectionIds types.List `tfsdk:"log_only_rule_collection_ids"` +} + type backendCredentials struct { AccessKey *string `tfsdk:"access_key_id"` SecretKey *string `tfsdk:"secret_access_key"` @@ -152,6 +188,9 @@ var configTypes = map[string]attr.Type{ "redirects": types.ObjectType{ AttrTypes: redirectsTypes, }, + "waf": types.ObjectType{ + AttrTypes: wafTypes, + }, } var optimizerTypes = map[string]attr.Type{ @@ -188,6 +227,24 @@ var redirectsTypes = map[string]attr.Type{ }, } +var wafTypes = map[string]attr.Type{ + "mode": types.StringType, + "type": types.StringType, + "paranoia_level": types.StringType, + "allowed_http_versions": types.ListType{ElemType: types.StringType}, + "allowed_request_content_types": types.ListType{ElemType: types.StringType}, + "allowed_http_methods": types.ListType{ElemType: types.StringType}, + "enabled_rule_ids": types.ListType{ElemType: types.StringType}, + "disabled_rule_ids": types.ListType{ElemType: types.StringType}, + "log_only_rule_ids": types.ListType{ElemType: types.StringType}, + "enabled_rule_group_ids": types.ListType{ElemType: types.StringType}, + "disabled_rule_group_ids": types.ListType{ElemType: types.StringType}, + "log_only_rule_group_ids": types.ListType{ElemType: types.StringType}, + "enabled_rule_collection_ids": types.ListType{ElemType: types.StringType}, + "disabled_rule_collection_ids": types.ListType{ElemType: types.StringType}, + "log_only_rule_collection_ids": types.ListType{ElemType: types.StringType}, +} + var backendTypes = map[string]attr.Type{ "type": types.StringType, "origin_url": types.StringType, @@ -401,6 +458,114 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, }, }, + "waf": schema.SingleNestedAttribute{ + Description: schemaDescriptions["config_waf"], + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "mode": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: schemaDescriptions["waf_mode"], + Default: stringdefault.StaticString("DISABLED"), + }, + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: schemaDescriptions["waf_type"], + Default: stringdefault.StaticString("FREE"), + }, + "paranoia_level": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: schemaDescriptions["waf_paranoia_level"], + }, + "allowed_http_versions": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_http_versions"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "allowed_request_content_types": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_request_content_types"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "allowed_http_methods": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_http_methods"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "enabled_rule_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "disabled_rule_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "log_only_rule_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "enabled_rule_group_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_group_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "disabled_rule_group_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_group_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "log_only_rule_group_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_group_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "enabled_rule_collection_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_collection_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "disabled_rule_collection_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_collection_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + "log_only_rule_collection_ids": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_collection_ids"], + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + }, + }, + }, "backend": schema.SingleNestedAttribute{ Required: true, Description: schemaDescriptions["config_backend"], @@ -792,6 +957,39 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Redirects: redirectsConfig, } + if !utils.IsUndefined(configModel.Waf) { + var wafModel wafConfig + diags := configModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") + return + } + + wafPatch := cdnSdk.WafConfigPatch{ + Mode: new(cdnSdk.WafMode(wafModel.Mode.ValueString())), + Type: new(cdnSdk.WafType(wafModel.Type.ValueString())), + AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions), + AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes), + AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods), + EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds), + DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds), + LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds), + EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds), + DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds), + LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds), + EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds), + DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds), + LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds), + } + + if !utils.IsUndefined(wafModel.ParanoiaLevel) { + pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString()) + wafPatch.ParanoiaLevel = &pl + } + + configPatch.Waf = &wafPatch + } + if !utils.IsUndefined(configModel.Optimizer) { var optimizerModel optimizerConfig @@ -1156,6 +1354,50 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo return core.DiagsToError(diags) } + // Map Waf + wafVal := types.ObjectNull(wafTypes) + wafObjAttrs := map[string]attr.Value{ + "mode": types.StringValue(string(distribution.Config.Waf.Mode)), + "type": types.StringValue(string(distribution.Config.Waf.Type)), + } + + if distribution.Config.Waf.ParanoiaLevel != nil { + wafObjAttrs["paranoia_level"] = types.StringValue(string(*distribution.Config.Waf.ParanoiaLevel)) + } else { + wafObjAttrs["paranoia_level"] = types.StringNull() + } + + wafObjAttrs["allowed_http_versions"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafListToHCL(distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleCollectionIds) + + // Prevent state drift if WAF wasn't in the config at all, but API returned default empty WAF + // By checking if the old config Waf block was null, we can avoid recreating a WAF block + // if the returned one matches the API default (FREE/DISABLED and empty lists). + isEmptyDefault := distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED && + distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE && + len(distribution.Config.Waf.AllowedHttpMethods) == 0 && + len(distribution.Config.Waf.EnabledRuleIds) == 0 + + if isEmptyDefault && oldConfig.Waf.IsNull() { + wafVal = types.ObjectNull(wafTypes) + } else { + var diagWaf diag.Diagnostics + wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) + if diagWaf.HasError() { + return core.DiagsToError(diagWaf) + } + } + optimizerVal := types.ObjectNull(optimizerTypes) if o := distribution.Config.Optimizer; o != nil { optimizerEnabled, ok := o.GetEnabledOk() @@ -1175,6 +1417,7 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo "blocked_countries": modelBlockedCountries, "optimizer": optimizerVal, "redirects": redirectsVal, + "waf": wafVal, }) if diags.HasError() { return core.DiagsToError(diags) @@ -1234,6 +1477,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut if cfg.Optimizer != nil { optimizer = cdnSdk.NewOptimizer(cfg.Optimizer.GetEnabled()) } + var backend *cdnSdk.CreateDistributionPayloadBackend if cfg.Backend.HttpBackend != nil { backend = &cdnSdk.CreateDistributionPayloadBackend{ @@ -1279,6 +1523,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut BlockedCountries: cfg.BlockedCountries, Optimizer: optimizer, Redirects: cfg.Redirects, + Waf: &cfg.Waf, } return payload, nil @@ -1393,6 +1638,36 @@ func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { Redirects: redirectsConfig, } + if !utils.IsUndefined(configModel.Waf) { + var wafModel wafConfig + diags := configModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + + cdnConfig.Waf = cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode(wafModel.Mode.ValueString()), + Type: cdnSdk.WafType(wafModel.Type.ValueString()), + AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions), + AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes), + AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods), + EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds), + DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds), + LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds), + EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds), + DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds), + LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds), + EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds), + DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds), + LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds), + } + + if !utils.IsUndefined(wafModel.ParanoiaLevel) { + pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString()) + cdnConfig.Waf.ParanoiaLevel = &pl + } + } + switch configModel.Backend.Type { case "http": originRequestHeaders := map[string]string{} @@ -1449,3 +1724,32 @@ func validateCountryCode(country string) (string, error) { return upperCountry, nil } + +// getSortedWafList extracts strings from HCL list, sorts them and returns the slice +func getSortedWafList(ctx context.Context, tfList basetypes.ListValue) []string { + if tfList.IsNull() || tfList.IsUnknown() { + return []string{} + } + var elements []string + diags := tfList.ElementsAs(ctx, &elements, true) + if diags.HasError() { + return []string{} + } + sort.Strings(elements) + return elements +} + +// mapWafListToHCL guarantees the returned HCL List is sorted +func mapWafListToHCL(apiList []string) basetypes.ListValue { + if len(apiList) == 0 { + return types.ListValueMust(types.StringType, []attr.Value{}) + } + sorted := make([]string, len(apiList)) + copy(sorted, apiList) + sort.Strings(sorted) + var elements []attr.Value + for _, val := range sorted { + elements = append(elements, types.StringValue(val)) + } + return types.ListValueMust(types.StringType, elements) +} diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index b6c64b20b..4649eba04 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -26,6 +26,7 @@ func TestToCreatePayload(t *testing.T) { geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{ "https://de.mycoolapp.com": geofencingCountries, }) + backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{ "type": types.StringValue("http"), "origin_url": types.StringValue("https://www.mycoolapp.com"), @@ -42,7 +43,40 @@ func TestToCreatePayload(t *testing.T) { optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(true), }) - + emptyWafList := types.ListValueMust(types.StringType, []attr.Value{}) + expectedDefaultWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("DISABLED"), + Type: cdnSdk.WafType("FREE"), + AllowedHttpVersions: []string{}, + AllowedRequestContentTypes: []string{}, + AllowedHttpMethods: []string{}, + EnabledRuleIds: []string{}, + DisabledRuleIds: []string{}, + LogOnlyRuleIds: []string{}, + EnabledRuleGroupIds: []string{}, + DisabledRuleGroupIds: []string{}, + LogOnlyRuleGroupIds: []string{}, + EnabledRuleCollectionIds: []string{}, + DisabledRuleCollectionIds: []string{}, + LogOnlyRuleCollectionIds: []string{}, + } + defaultWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("DISABLED"), + "type": types.StringValue("FREE"), + "paranoia_level": types.StringNull(), + "allowed_http_versions": emptyWafList, + "allowed_request_content_types": emptyWafList, + "allowed_http_methods": emptyWafList, + "enabled_rule_ids": emptyWafList, + "disabled_rule_ids": emptyWafList, + "log_only_rule_ids": emptyWafList, + "enabled_rule_group_ids": emptyWafList, + "disabled_rule_group_ids": emptyWafList, + "log_only_rule_group_ids": emptyWafList, + "enabled_rule_collection_ids": emptyWafList, + "disabled_rule_collection_ids": emptyWafList, + "log_only_rule_collection_ids": emptyWafList, + }) redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) if !ok { t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") @@ -55,6 +89,7 @@ func TestToCreatePayload(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": defaultWaf, }) matcherValues := types.ListValueMust(types.StringType, []attr.Value{ @@ -79,6 +114,46 @@ func TestToCreatePayload(t *testing.T) { redirectsConfigVal := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{ "rules": rulesList, }) + populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafList, + "allowed_request_content_types": populatedWafList, + "allowed_http_methods": populatedWafList, + "enabled_rule_ids": populatedWafList, + "disabled_rule_ids": populatedWafList, + "log_only_rule_ids": populatedWafList, + "enabled_rule_group_ids": populatedWafList, + "disabled_rule_group_ids": populatedWafList, + "log_only_rule_group_ids": populatedWafList, + "enabled_rule_collection_ids": populatedWafList, + "disabled_rule_collection_ids": populatedWafList, + "log_only_rule_collection_ids": populatedWafList, + }) + + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } modelFixture := func(mods ...func(*Model)) *Model { model := &Model{ @@ -109,6 +184,7 @@ func TestToCreatePayload(t *testing.T) { Type: "http", }, }, + Waf: &expectedDefaultWafConfig, }, IsValid: true, }, @@ -120,12 +196,14 @@ func TestToCreatePayload(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": defaultWaf, }) }), Expected: &cdnSdk.CreateDistributionPayload{ Regions: []cdnSdk.Region{"EU", "US"}, Optimizer: cdnSdk.NewOptimizer(true), BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: &expectedDefaultWafConfig, Backend: cdnSdk.CreateDistributionPayloadBackend{ HttpBackendCreate: &cdnSdk.HttpBackendCreate{ Geofencing: &map[string][]string{"https://de.mycoolapp.com": {"DE", "FR"}}, @@ -145,11 +223,13 @@ func TestToCreatePayload(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigVal, + "waf": defaultWaf, }) }), Expected: &cdnSdk.CreateDistributionPayload{ Regions: []cdnSdk.Region{"EU", "US"}, BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: &expectedDefaultWafConfig, Backend: cdnSdk.CreateDistributionPayloadBackend{ HttpBackendCreate: &cdnSdk.HttpBackendCreate{ Geofencing: &map[string][]string{"https://de.mycoolapp.com": {"DE", "FR"}}, @@ -199,9 +279,11 @@ func TestToCreatePayload(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": defaultWaf, }) }), Expected: &cdnSdk.CreateDistributionPayload{ + Waf: &expectedDefaultWafConfig, Backend: cdnSdk.CreateDistributionPayloadBackend{ BucketBackendCreate: &cdnSdk.BucketBackendCreate{ Type: "bucket", @@ -218,6 +300,32 @@ func TestToCreatePayload(t *testing.T) { }, IsValid: true, }, + "happy_path_with_waf": { + 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": types.ObjectNull(redirectsAttrTypes), + "waf": populatedWaf, + }) + }), + Expected: &cdnSdk.CreateDistributionPayload{ + Regions: []cdnSdk.Region{"EU", "US"}, + BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: &expectedWafConfig, + Backend: cdnSdk.CreateDistributionPayloadBackend{ + HttpBackendCreate: &cdnSdk.HttpBackendCreate{ + Geofencing: &map[string][]string{"https://de.mycoolapp.com": {"DE", "FR"}}, + OriginRequestHeaders: &map[string]string{"testHeader0": "testHeaderValue0", "testHeader1": "testHeaderValue1"}, + OriginUrl: "https://www.mycoolapp.com", + Type: "http", + }, + }, + }, + IsValid: true, + }, "sad_path_model_nil": { Input: nil, Expected: nil, @@ -293,6 +401,7 @@ func TestConvertConfig(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) matcherValues := types.ListValueMust(types.StringType, []attr.Value{ @@ -313,7 +422,46 @@ func TestConvertConfig(t *testing.T) { "matchers": matchersList, }) rulesList := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleVal}) + populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafList, + "allowed_request_content_types": populatedWafList, + "allowed_http_methods": populatedWafList, + "enabled_rule_ids": populatedWafList, + "disabled_rule_ids": populatedWafList, + "log_only_rule_ids": populatedWafList, + "enabled_rule_group_ids": populatedWafList, + "disabled_rule_group_ids": populatedWafList, + "log_only_rule_group_ids": populatedWafList, + "enabled_rule_collection_ids": populatedWafList, + "disabled_rule_collection_ids": populatedWafList, + "log_only_rule_collection_ids": populatedWafList, + }) + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } redirectsConfigVal := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{ "rules": rulesList, }) @@ -364,6 +512,7 @@ func TestConvertConfig(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) }), Expected: &cdnSdk.Config{ @@ -385,6 +534,36 @@ func TestConvertConfig(t *testing.T) { BlockedCountries: []string{"XX", "YY", "ZZ"}, }, IsValid: true, + }, "happy_path_with_waf": { + 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": types.ObjectNull(redirectsAttrTypes), + "waf": populatedWaf, + }) + }), + Expected: &cdnSdk.Config{ + Backend: cdnSdk.ConfigBackend{ + HttpBackend: &cdnSdk.HttpBackend{ + OriginRequestHeaders: map[string]string{ + "testHeader0": "testHeaderValue0", + "testHeader1": "testHeaderValue1", + }, + OriginUrl: "https://www.mycoolapp.com", + Type: "http", + Geofencing: map[string][]string{ + "https://de.mycoolapp.com": {"DE", "FR"}, + }, + }, + }, + Regions: []cdnSdk.Region{"EU", "US"}, + BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: expectedWafConfig, + }, + IsValid: true, }, "happy_path_with_redirects": { Input: modelFixture(func(m *Model) { @@ -394,6 +573,7 @@ func TestConvertConfig(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigVal, // Injetando o mock aqui + "waf": types.ObjectNull(wafTypes), }) }), Expected: &cdnSdk.Config{ @@ -453,6 +633,7 @@ func TestConvertConfig(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) }), Expected: &cdnSdk.Config{ @@ -545,13 +726,53 @@ func TestMapFields(t *testing.T) { t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") } redirectsAttrTypes := redirectsObjType.AttrTypes + populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafList, + "allowed_request_content_types": populatedWafList, + "allowed_http_methods": populatedWafList, + "enabled_rule_ids": populatedWafList, + "disabled_rule_ids": populatedWafList, + "log_only_rule_ids": populatedWafList, + "enabled_rule_group_ids": populatedWafList, + "disabled_rule_group_ids": populatedWafList, + "log_only_rule_group_ids": populatedWafList, + "enabled_rule_collection_ids": populatedWafList, + "disabled_rule_collection_ids": populatedWafList, + "log_only_rule_collection_ids": populatedWafList, + }) + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) redirectsInput := &cdnSdk.RedirectConfig{ @@ -636,6 +857,10 @@ func TestMapFields(t *testing.T) { Regions: []cdnSdk.Region{"EU", "US"}, BlockedCountries: []string{"XX", "YY", "ZZ"}, Optimizer: nil, + Waf: cdnSdk.WafConfig{ + Mode: cdnSdk.WAFMODE_DISABLED, + Type: cdnSdk.WAFTYPE_FREE, + }, }, CreatedAt: createdAt, Domains: []cdnSdk.Domain{ @@ -675,6 +900,7 @@ func TestMapFields(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) tests := map[string]struct { Input *cdnSdk.Distribution @@ -695,6 +921,7 @@ func TestMapFields(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -721,6 +948,7 @@ func TestMapFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), // <-- Change this }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -736,6 +964,7 @@ func TestMapFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigExpected, + "waf": types.ObjectNull(wafTypes), // <-- Change this }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -751,6 +980,21 @@ func TestMapFields(t *testing.T) { d.Status = "ERROR" }), IsValid: true, + }, "happy_path_with_waf": { + 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": types.ObjectNull(redirectsAttrTypes), + "waf": populatedWaf, + }) + }), + Input: distributionFixture(func(d *cdnSdk.Distribution) { + d.Config.Waf = expectedWafConfig + }), + IsValid: true, }, "happy_path_custom_domain": { Expected: expectedModel(func(m *Model) { From 731fb3ff1778712fb8dbcef4f1d4f2c888a23602 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Sun, 12 Apr 2026 19:33:39 +0200 Subject: [PATCH 14/62] feat: add waf in datasource --- .../services/cdn/distribution/datasource.go | 117 ++++++++++++++++++ .../cdn/distribution/datasource_test.go | 66 ++++++++++ 2 files changed, 183 insertions(+) diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index ec1bba135..cfe5598b5 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -41,6 +41,9 @@ var dataSourceConfigTypes = map[string]attr.Type{ "redirects": types.ObjectType{ AttrTypes: redirectsTypes, // Shared from resource.go }, + "waf": types.ObjectType{ + AttrTypes: wafTypes, // Shared from resource.go + }, } type distributionDataSource struct { @@ -253,6 +256,84 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe }, }, }, + "waf": schema.SingleNestedAttribute{ + Description: schemaDescriptions["config_waf"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "mode": schema.StringAttribute{ + Computed: true, + Description: schemaDescriptions["waf_mode"], + }, + "type": schema.StringAttribute{ + Computed: true, + Description: schemaDescriptions["waf_type"], + }, + "paranoia_level": schema.StringAttribute{ + Computed: true, + Description: schemaDescriptions["waf_paranoia_level"], + }, + "allowed_http_versions": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_http_versions"], + }, + "allowed_request_content_types": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_request_content_types"], + }, + "allowed_http_methods": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_http_methods"], + }, + "enabled_rule_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_ids"], + }, + "disabled_rule_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_ids"], + }, + "log_only_rule_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_ids"], + }, + "enabled_rule_group_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_group_ids"], + }, + "disabled_rule_group_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_group_ids"], + }, + "log_only_rule_group_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_group_ids"], + }, + "enabled_rule_collection_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_collection_ids"], + }, + "disabled_rule_collection_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_collection_ids"], + }, + "log_only_rule_collection_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_collection_ids"], + }, + }, + }, }, }, }, @@ -510,6 +591,41 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, return core.DiagsToError(diags) } + // Map Waf + wafVal := types.ObjectNull(wafTypes) + if distribution.Config.Waf.Mode != "" { + wafObjAttrs := map[string]attr.Value{ + "mode": types.StringValue(string(distribution.Config.Waf.Mode)), + "type": types.StringValue(string(distribution.Config.Waf.Type)), + } + + if distribution.Config.Waf.ParanoiaLevel != nil { + wafObjAttrs["paranoia_level"] = types.StringValue(string(*distribution.Config.Waf.ParanoiaLevel)) + } else { + wafObjAttrs["paranoia_level"] = types.StringNull() + } + + // Uses the mapWafListToHCL defined in resource.go + wafObjAttrs["allowed_http_versions"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafListToHCL(distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleCollectionIds) + + var diagWaf diag.Diagnostics + wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) + if diagWaf.HasError() { + return core.DiagsToError(diagWaf) + } + } + // Optimizer optimizerVal := types.ObjectNull(optimizerTypes) if o := distribution.Config.Optimizer; o != nil { @@ -531,6 +647,7 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, "blocked_countries": modelBlockedCountries, "optimizer": optimizerVal, "redirects": redirectsVal, + "waf": wafVal, }) 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 df8440f4b..7aac342b6 100644 --- a/stackit/internal/services/cdn/distribution/datasource_test.go +++ b/stackit/internal/services/cdn/distribution/datasource_test.go @@ -52,6 +52,7 @@ func TestMapDataSourceFields(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) redirectsInput := cdnSdk.RedirectConfig{ Rules: []cdnSdk.RedirectRule{ @@ -100,6 +101,49 @@ func TestMapDataSourceFields(t *testing.T) { "errors": types.ListValueMust(types.StringType, []attr.Value{}), }) domains := types.ListValueMust(types.ObjectType{AttrTypes: domainTypes}, []attr.Value{managedDomain}) + + // WAF Fixtures + populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafList, + "allowed_request_content_types": populatedWafList, + "allowed_http_methods": populatedWafList, + "enabled_rule_ids": populatedWafList, + "disabled_rule_ids": populatedWafList, + "log_only_rule_ids": populatedWafList, + "enabled_rule_group_ids": populatedWafList, + "disabled_rule_group_ids": populatedWafList, + "log_only_rule_group_ids": populatedWafList, + "enabled_rule_collection_ids": populatedWafList, + "disabled_rule_collection_ids": populatedWafList, + "log_only_rule_collection_ids": populatedWafList, + }) + + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } + expectedModel := func(mods ...func(*Model)) *Model { model := &Model{ ID: types.StringValue("test-project-id,test-distribution-id"), @@ -117,6 +161,7 @@ func TestMapDataSourceFields(t *testing.T) { } return model } + distributionFixture := func(mods ...func(*cdnSdk.Distribution)) *cdnSdk.Distribution { distribution := &cdnSdk.Distribution{ Config: cdnSdk.Config{ @@ -161,6 +206,7 @@ func TestMapDataSourceFields(t *testing.T) { "origin_request_headers": types.MapNull(types.StringType), "geofencing": types.MapNull(geofencingTypes.ElemType), }) + tests := map[string]struct { Input *cdnSdk.Distribution Expected *Model @@ -179,6 +225,7 @@ func TestMapDataSourceFields(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -205,6 +252,7 @@ func TestMapDataSourceFields(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) }), IsValid: true, @@ -225,6 +273,7 @@ func TestMapDataSourceFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -249,6 +298,7 @@ func TestMapDataSourceFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigExpected, + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -256,6 +306,22 @@ func TestMapDataSourceFields(t *testing.T) { }), IsValid: true, }, + "happy_path_with_waf": { + 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": types.ObjectNull(redirectsAttrTypes), + "waf": populatedWaf, + }) + }), + Input: distributionFixture(func(d *cdnSdk.Distribution) { + d.Config.Waf = expectedWafConfig + }), + IsValid: true, + }, "happy_path_custom_domain": { Expected: expectedModel(func(m *Model) { managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{ From 162d0d8f703fbe60b7db33e72f0018fee8071492 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 13 Apr 2026 10:54:00 +0200 Subject: [PATCH 15/62] fix acc test --- stackit/internal/services/cdn/cdn_acc_test.go | 3 + .../services/cdn/distribution/resource.go | 71 ++++++++++++++----- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 293ef0962..644833358 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -94,6 +94,9 @@ var testConfigVarsHttp = config.Variables{ "redirect_target_url": config.StringVariable("https://example.com"), "redirect_status_code": config.IntegerVariable(301), "redirect_matcher_value": config.StringVariable("/shop/*"), + "waf_mode": config.StringVariable("LOG_ONLY"), + "waf_type": config.StringVariable("PREMIUM"), + "waf_enabled_rule_ids": config.ListVariable(config.StringVariable("@builtin/crs/request/941120")), } func configVarsHttpUpdated() config.Variables { diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 2f90e8c88..0cc010d3c 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -304,6 +304,20 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques backendOptions := []string{"http", "bucket"} matchCondition := []string{"ANY", "ALL", "NONE"} statusCode := []int32{301, 302, 303, 307, 308} + defaultWafConfigAllowedHttpVersions := sortedStringListToAttrValueList([]string{ + "HTTP/1.0", "HTTP/1.1", "HTTP/2", "HTTP/2.0", + }) + defaultWafConfigAllowedRequestContentTypes := sortedStringListToAttrValueList([]string{ + "application/x-www-form-urlencoded", "multipart/form-data", "multipart/related", + "text/xml", "application/xml", "application/soap+xml", "application/x-amf", + "application/json", "application/octet-stream", "application/csp-report", + "application/xss-auditor-report", "text/plain", + }) + defaultWafConfigAllowedHttpMethods := sortedStringListToAttrValueList([]string{ + "GET", "HEAD", "POST", "PUT", "DELETE", + "CONNECT", "OPTIONS", "TRACE", "PATCH", + }) + resp.Schema = schema.Schema{ MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource), Description: "CDN distribution data source schema.", @@ -485,21 +499,21 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_versions"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedHttpVersions)), }, "allowed_request_content_types": schema.ListAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_request_content_types"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedRequestContentTypes)), }, "allowed_http_methods": schema.ListAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_methods"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), + Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedHttpMethods)), }, "enabled_rule_ids": schema.ListAttribute{ Optional: true, @@ -1385,10 +1399,9 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo // if the returned one matches the API default (FREE/DISABLED and empty lists). isEmptyDefault := distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED && distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE && - len(distribution.Config.Waf.AllowedHttpMethods) == 0 && len(distribution.Config.Waf.EnabledRuleIds) == 0 - if isEmptyDefault && oldConfig.Waf.IsNull() { + if isEmptyDefault && utils.IsUndefined(oldConfig.Waf) { wafVal = types.ObjectNull(wafTypes) } else { var diagWaf diag.Diagnostics @@ -1469,10 +1482,21 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut if model == nil { return nil, fmt.Errorf("missing model") } + + var rawConfig distributionConfig + diags := model.Config.As(ctx, &rawConfig, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: false, + UnhandledUnknownAsEmpty: false, + }) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + cfg, err := convertConfig(ctx, model) if err != nil { return nil, err } + var optimizer *cdnSdk.Optimizer if cfg.Optimizer != nil { optimizer = cdnSdk.NewOptimizer(cfg.Optimizer.GetEnabled()) @@ -1489,16 +1513,6 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut }, } } else if cfg.Backend.BucketBackend != nil { - // We need to parse the model again to access the credentials, - // as convertConfig returns the SDK Config struct which hides them. - var rawConfig distributionConfig - diags := model.Config.As(ctx, &rawConfig, basetypes.ObjectAsOptions{ - UnhandledNullAsEmpty: false, - UnhandledUnknownAsEmpty: false, - }) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } var accessKey, secretKey *string if rawConfig.Backend.Credentials != nil { accessKey = rawConfig.Backend.Credentials.AccessKey @@ -1516,6 +1530,13 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut }, } } + + // Conditionally set the WAF payload to nil if it's not defined + var wafPayload *cdnSdk.WafConfig + if !utils.IsUndefined(rawConfig.Waf) { + wafPayload = &cfg.Waf + } + payload := &cdnSdk.CreateDistributionPayload{ IntentId: new(uuid.NewString()), Regions: cfg.Regions, @@ -1523,7 +1544,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut BlockedCountries: cfg.BlockedCountries, Optimizer: optimizer, Redirects: cfg.Redirects, - Waf: &cfg.Waf, + Waf: wafPayload, // Now passes nil if omitted } return payload, nil @@ -1728,7 +1749,7 @@ func validateCountryCode(country string) (string, error) { // getSortedWafList extracts strings from HCL list, sorts them and returns the slice func getSortedWafList(ctx context.Context, tfList basetypes.ListValue) []string { if tfList.IsNull() || tfList.IsUnknown() { - return []string{} + return nil } var elements []string diags := tfList.ElementsAs(ctx, &elements, true) @@ -1753,3 +1774,19 @@ func mapWafListToHCL(apiList []string) basetypes.ListValue { } return types.ListValueMust(types.StringType, elements) } + +// sortedStringListToAttrValueList sorts a slice of strings and converts it +// to a slice of attr.Value for use in Terraform schema defaults. +func sortedStringListToAttrValueList(items []string) []attr.Value { + sortedItems := make([]string, len(items)) + copy(sortedItems, items) + + sort.Strings(sortedItems) + + attrValues := make([]attr.Value, len(sortedItems)) + for i, val := range sortedItems { + attrValues[i] = types.StringValue(val) + } + + return attrValues +} From bd94faa9e278ce8f878f32eee63794a791951608 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 13 Apr 2026 14:32:24 +0200 Subject: [PATCH 16/62] feat: improve waf to avoid state drift --- docs/data-sources/cdn_distribution.md | 23 +++ docs/resources/cdn_distribution.md | 23 +++ .../services/cdn/distribution/resource.go | 153 +++++++++++------- .../cdn/testdata/resource-http-base.tf | 8 + 4 files changed, 150 insertions(+), 57 deletions(-) diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 099a24799..4ff668f24 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -53,6 +53,7 @@ Read-Only: - `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 +- `waf` (Attributes) Configuration of the Web Application Firewall (WAF) for the distribution. Removing this block from your configuration will completely disable the WAF. (see [below for nested schema](#nestedatt--config--waf)) ### Nested Schema for `config.backend` @@ -105,6 +106,28 @@ Read-Only: + +### Nested Schema for `config.waf` + +Read-Only: + +- `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. +- `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. +- `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. +- `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. +- `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. + + ### Nested Schema for `domains` diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index f60345443..47f591bbe 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -115,6 +115,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)) +- `waf` (Attributes) Configuration of the Web Application Firewall (WAF) for the distribution. Removing this block from your configuration will completely disable the WAF. (see [below for nested schema](#nestedatt--config--waf)) ### Nested Schema for `config.backend` @@ -186,6 +187,28 @@ Optional: + +### Nested Schema for `config.waf` + +Optional: + +- `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. +- `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. +- `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. +- `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. +- `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. + + ### Nested Schema for `domains` diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 0cc010d3c..a561a328b 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -84,22 +84,22 @@ var schemaDescriptions = map[string]string{ "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'.", - "config_waf": "Configuration of the WAF of a distribution.", - "waf_mode": "The WAF mode. ENABLED actively blocks, LOG_ONLY logs matches but never blocks, DISABLED completely turns off inspection.", - "waf_type": "Enable or disable the Premium WAF. FREE or PREMIUM.", - "waf_paranoia_level": "Defines how aggressively the WAF should action on requests (L1 to L4).", - "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted.", - "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies.", - "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts.", - "waf_enabled_rule_ids": "Ids of the WAF rules explicitly enabled.", - "waf_disabled_rule_ids": "Ids of WAF Rules explicitly disabled.", - "waf_log_only_rule_ids": "Ids of WAF Rules explicitly marked as Log Only.", - "waf_enabled_rule_group_ids": "Ids of WAF Rule Groups explicitly enabled.", - "waf_disabled_rule_group_ids": "Ids of WAF Rule Groups explicitly disabled.", - "waf_log_only_rule_group_ids": "Ids of WAF Rule Groups explicitly marked as log Only.", - "waf_enabled_rule_collection_ids": "Ids of WAF Collections explicitly enabled.", - "waf_disabled_rule_collection_ids": "Ids of WAF Collections explicitly disabled.", - "waf_log_only_rule_collection_ids": "Ids of WAF Collections explicitly marked as log Only.", + "config_waf": "Configuration of the Web Application Firewall (WAF) for the distribution. Removing this block from your configuration will completely disable the WAF.", + "waf_mode": "The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'.", + "waf_type": "The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'.", + "waf_paranoia_level": "Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'.", + "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`.", + "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`.", + "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`.", + "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", } type Model struct { @@ -493,6 +493,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Optional: true, Computed: true, Description: schemaDescriptions["waf_paranoia_level"], + Default: stringdefault.StaticString("L1"), }, "allowed_http_versions": schema.ListAttribute{ Optional: true, @@ -826,8 +827,15 @@ func (r *distributionResource) Read(ctx context.Context, req resource.ReadReques } func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) + var planModel Model + diags := req.Plan.Get(ctx, &planModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var stateModel Model + diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -835,23 +843,33 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe ctx = core.InitProviderContext(ctx) - projectId := model.ProjectId.ValueString() - distributionId := model.DistributionId.ValueString() + projectId := planModel.ProjectId.ValueString() + distributionId := planModel.DistributionId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "distribution_id", distributionId) - configModel := distributionConfig{} - diags = model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{ + configPlanModel := distributionConfig{} + diags = planModel.Config.As(ctx, &configPlanModel, basetypes.ObjectAsOptions{ UnhandledNullAsEmpty: false, UnhandledUnknownAsEmpty: false, }) if diags.HasError() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping config") + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping plan config") + return + } + + configStateModel := distributionConfig{} + diags = stateModel.Config.As(ctx, &configStateModel, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: false, + UnhandledUnknownAsEmpty: false, + }) + if diags.HasError() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping state config") return } regions := []cdnSdk.Region{} - for _, r := range *configModel.Regions { + for _, r := range *configPlanModel.Regions { regionEnum, err := cdnSdk.NewRegionFromValue(r) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Map regions: %v", err)) @@ -861,13 +879,10 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } // blockedCountries - // Use a pointer to a slice to distinguish between an empty list (unblock all) and nil (no change). var blockedCountries []string - if configModel.BlockedCountries != nil { - // Use a temporary slice + if configPlanModel.BlockedCountries != nil { tempBlockedCountries := []string{} - - for _, blockedCountry := range *configModel.BlockedCountries { + for _, blockedCountry := range *configPlanModel.BlockedCountries { validatedBlockedCountry, err := validateCountryCode(blockedCountry) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Blocked countries: %v", err)) @@ -875,17 +890,15 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } tempBlockedCountries = append(tempBlockedCountries, validatedBlockedCountry) } - - // Point to the populated slice blockedCountries = tempBlockedCountries } // redirects var redirectsConfig *cdnSdk.RedirectConfig - if configModel.Redirects != nil { + if configPlanModel.Redirects != nil { sdkRules := []cdnSdk.RedirectRule{} - if len(configModel.Redirects.Rules) > 0 { - for _, rule := range configModel.Redirects.Rules { + if len(configPlanModel.Redirects.Rules) > 0 { + for _, rule := range configPlanModel.Redirects.Rules { matchers := []cdnSdk.Matcher{} for _, matcher := range rule.Matchers { var matchCond *cdnSdk.MatchCondition @@ -925,12 +938,12 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe configPatchBackend := &cdnSdk.ConfigPatchBackend{} - switch configModel.Backend.Type { + switch configPlanModel.Backend.Type { case "http": geofencingPatch := map[string][]string{} - if configModel.Backend.Geofencing != nil { + if configPlanModel.Backend.Geofencing != nil { gf := make(map[string][]string) - for url, countries := range *configModel.Backend.Geofencing { + for url, countries := range *configPlanModel.Backend.Geofencing { countryStrings := make([]string, len(countries)) for i, countryPtr := range countries { if countryPtr == nil { @@ -945,21 +958,21 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } configPatchBackend.HttpBackendPatch = &cdnSdk.HttpBackendPatch{ - OriginRequestHeaders: configModel.Backend.OriginRequestHeaders, - OriginUrl: configModel.Backend.OriginURL, + OriginRequestHeaders: configPlanModel.Backend.OriginRequestHeaders, + OriginUrl: configPlanModel.Backend.OriginURL, Type: "http", Geofencing: &geofencingPatch, } case "bucket": configPatchBackend.BucketBackendPatch = &cdnSdk.BucketBackendPatch{ Type: "bucket", - BucketUrl: configModel.Backend.BucketURL, - Region: configModel.Backend.Region, + BucketUrl: configPlanModel.Backend.BucketURL, + Region: configPlanModel.Backend.Region, } - if configModel.Backend.Credentials != nil { + if configPlanModel.Backend.Credentials != nil { configPatchBackend.BucketBackendPatch.Credentials = &cdnSdk.BucketCredentials{ - AccessKeyId: *configModel.Backend.Credentials.AccessKey, - SecretAccessKey: *configModel.Backend.Credentials.SecretKey, + AccessKeyId: *configPlanModel.Backend.Credentials.AccessKey, + SecretAccessKey: *configPlanModel.Backend.Credentials.SecretKey, } } } @@ -971,9 +984,10 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Redirects: redirectsConfig, } - if !utils.IsUndefined(configModel.Waf) { + // Map WAF Update/Removal + if !utils.IsUndefined(configPlanModel.Waf) { var wafModel wafConfig - diags := configModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) + diags := configPlanModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) if diags.HasError() { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") return @@ -1001,13 +1015,39 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe wafPatch.ParanoiaLevel = &pl } + configPatch.Waf = &wafPatch + + } else if !utils.IsUndefined(configStateModel.Waf) { + // User explicitly removed the WAF block from their terraform configuration + modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED) + typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE) + + wafPatch := cdnSdk.WafConfigPatch{ + Mode: &modeDisabled, + Type: &typeFree, + + // Send empty arrays to clear rules, keeping the API happy + EnabledRuleIds: []string{}, + DisabledRuleIds: []string{}, + LogOnlyRuleIds: []string{}, + EnabledRuleGroupIds: []string{}, + DisabledRuleGroupIds: []string{}, + LogOnlyRuleGroupIds: []string{}, + EnabledRuleCollectionIds: []string{}, + DisabledRuleCollectionIds: []string{}, + LogOnlyRuleCollectionIds: []string{}, + + // Intentionally omitted (nil) to avoid the 422 Unprocessable Entity error: + // AllowedHttpVersions, AllowedRequestContentTypes, AllowedHttpMethods + } + configPatch.Waf = &wafPatch } - if !utils.IsUndefined(configModel.Optimizer) { + if !utils.IsUndefined(configPlanModel.Optimizer) { var optimizerModel optimizerConfig - diags = configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) + diags = configPlanModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) if diags.HasError() { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping optimizer config") return @@ -1037,13 +1077,13 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe return } - err = mapFields(ctx, &waitResp.Distribution, &model) + err = mapFields(ctx, &waitResp.Distribution, &planModel) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Processing API payload: %v", err)) return } - diags = resp.State.Set(ctx, model) + diags = resp.State.Set(ctx, planModel) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -1394,14 +1434,13 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo wafObjAttrs["disabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleCollectionIds) wafObjAttrs["log_only_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleCollectionIds) - // Prevent state drift if WAF wasn't in the config at all, but API returned default empty WAF - // By checking if the old config Waf block was null, we can avoid recreating a WAF block - // if the returned one matches the API default (FREE/DISABLED and empty lists). - isEmptyDefault := distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED && - distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE && - len(distribution.Config.Waf.EnabledRuleIds) == 0 + // Safely determine if the API considers the WAF completely disabled + isWafDisabled := distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED && + distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE - if isEmptyDefault && utils.IsUndefined(oldConfig.Waf) { + // If the WAF is disabled in the API, and there wasn't a WAF block in the user's previous state, + // keep it null to prevent state drift from residual backend default values. + if isWafDisabled && utils.IsUndefined(oldConfig.Waf) { wafVal = types.ObjectNull(wafTypes) } else { var diagWaf diag.Diagnostics diff --git a/stackit/internal/services/cdn/testdata/resource-http-base.tf b/stackit/internal/services/cdn/testdata/resource-http-base.tf index 026d1b990..9eb97dd71 100644 --- a/stackit/internal/services/cdn/testdata/resource-http-base.tf +++ b/stackit/internal/services/cdn/testdata/resource-http-base.tf @@ -12,6 +12,9 @@ variable "private_key" {} variable "redirect_target_url" {} variable "redirect_status_code" {} variable "redirect_matcher_value" {} +variable "waf_mode" {} +variable "waf_type" {} +variable "waf_enabled_rule_ids" {} # dns variable "dns_zone_name" {} @@ -55,6 +58,11 @@ resource "stackit_cdn_distribution" "distribution" { } ] } + waf = { + mode = var.waf_mode + type = var.waf_type + enabled_rule_ids = var.waf_enabled_rule_ids + } backend = { type = var.backend_http_type origin_url = var.backend_origin_url From 312cca03bf8c68e0460897873e77c8dbbbb4e62b Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 13 Apr 2026 14:45:37 +0200 Subject: [PATCH 17/62] fyi: run fmt and fix linter issues --- stackit/internal/services/cdn/distribution/resource.go | 9 +-------- .../internal/services/cdn/testdata/resource-http-base.tf | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index a561a328b..af6c0518c 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -992,7 +992,6 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") return } - wafPatch := cdnSdk.WafConfigPatch{ Mode: new(cdnSdk.WafMode(wafModel.Mode.ValueString())), Type: new(cdnSdk.WafType(wafModel.Type.ValueString())), @@ -1009,14 +1008,11 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds), LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds), } - if !utils.IsUndefined(wafModel.ParanoiaLevel) { pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString()) wafPatch.ParanoiaLevel = &pl } - configPatch.Waf = &wafPatch - } else if !utils.IsUndefined(configStateModel.Waf) { // User explicitly removed the WAF block from their terraform configuration modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED) @@ -1025,7 +1021,6 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe wafPatch := cdnSdk.WafConfigPatch{ Mode: &modeDisabled, Type: &typeFree, - // Send empty arrays to clear rules, keeping the API happy EnabledRuleIds: []string{}, DisabledRuleIds: []string{}, @@ -1036,11 +1031,9 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe EnabledRuleCollectionIds: []string{}, DisabledRuleCollectionIds: []string{}, LogOnlyRuleCollectionIds: []string{}, - // Intentionally omitted (nil) to avoid the 422 Unprocessable Entity error: // AllowedHttpVersions, AllowedRequestContentTypes, AllowedHttpMethods } - configPatch.Waf = &wafPatch } @@ -1409,7 +1402,6 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo } // Map Waf - wafVal := types.ObjectNull(wafTypes) wafObjAttrs := map[string]attr.Value{ "mode": types.StringValue(string(distribution.Config.Waf.Mode)), "type": types.StringValue(string(distribution.Config.Waf.Type)), @@ -1440,6 +1432,7 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo // If the WAF is disabled in the API, and there wasn't a WAF block in the user's previous state, // keep it null to prevent state drift from residual backend default values. + var wafVal attr.Value if isWafDisabled && utils.IsUndefined(oldConfig.Waf) { wafVal = types.ObjectNull(wafTypes) } else { diff --git a/stackit/internal/services/cdn/testdata/resource-http-base.tf b/stackit/internal/services/cdn/testdata/resource-http-base.tf index 9eb97dd71..ffe89606b 100644 --- a/stackit/internal/services/cdn/testdata/resource-http-base.tf +++ b/stackit/internal/services/cdn/testdata/resource-http-base.tf @@ -59,8 +59,8 @@ resource "stackit_cdn_distribution" "distribution" { ] } waf = { - mode = var.waf_mode - type = var.waf_type + mode = var.waf_mode + type = var.waf_type enabled_rule_ids = var.waf_enabled_rule_ids } backend = { From a15c7f74b4a49b67ce56def428c5503bd685de87 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 13 Apr 2026 15:18:00 +0200 Subject: [PATCH 18/62] feat: add doc and improve descriptions --- docs/data-sources/cdn_distribution.md | 18 +++--- docs/resources/cdn_distribution.md | 56 +++++++++++++++---- .../stackit_cdn_distribution/resource.tf | 33 +++++++++++ .../services/cdn/distribution/resource.go | 22 ++++---- 4 files changed, 98 insertions(+), 31 deletions(-) diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 4ff668f24..ac6ad8de1 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -114,15 +114,15 @@ Read-Only: - `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. - `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. - `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. - `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index 47f591bbe..72f3ba49d 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -74,6 +74,39 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { } ] } + +# WAF Configuration + # + # Precedence Hierarchy: Specific Rules > Groups > Collections + # In this example, the entire "@builtin/crs/request" collection is ENABLED. + # However, because specific Rule IDs have a higher precedence, the rule + # "@builtin/crs/request/942151" is explicitly DISABLED, overriding the collection setting. + # + # To view all available collections, groups, and rules, consult the API documentation: + # https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections + waf = { + mode = "ENABLED" + type = "PREMIUM" + paranoia_level = "L1" + allowed_http_versions = ["HTTP/1.0", "HTTP/1.1"] + allowed_http_methods = ["GET"] + allowed_request_content_types = ["text/plain"] + + # Collections + enabled_rule_collection_ids = ["@builtin/crs/request"] + disabled_rule_collection_ids = [] + log_only_rule_collection_ids = ["@builtin/crs/response"] + + # Groups + enabled_rule_group_ids = [] + disabled_rule_group_ids = [] + log_only_rule_group_ids = [] + + # Specific Rules (Highest Precedence) + enabled_rule_ids = ["@builtin/crs/request/913100"] + disabled_rule_ids = ["@builtin/crs/request/942151"] + log_only_rule_ids = ["@builtin/crs/response/954120"] + } } } @@ -190,21 +223,24 @@ Optional: ### Nested Schema for `config.waf` +Required: + +- `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. + Optional: - `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. - `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. - `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. - `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. diff --git a/examples/resources/stackit_cdn_distribution/resource.tf b/examples/resources/stackit_cdn_distribution/resource.tf index 4c37818bf..c532b0ee0 100644 --- a/examples/resources/stackit_cdn_distribution/resource.tf +++ b/examples/resources/stackit_cdn_distribution/resource.tf @@ -56,6 +56,39 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { } ] } + + # WAF Configuration + # + # Precedence Hierarchy: Specific Rules > Groups > Collections + # In this example, the entire "@builtin/crs/request" collection is ENABLED. + # However, because specific Rule IDs have a higher precedence, the rule + # "@builtin/crs/request/942151" is explicitly DISABLED, overriding the collection setting. + # + # To view all available collections, groups, and rules, consult the API documentation: + # https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections + waf = { + mode = "ENABLED" + type = "PREMIUM" + paranoia_level = "L1" + allowed_http_versions = ["HTTP/1.0", "HTTP/1.1"] + allowed_http_methods = ["GET"] + allowed_request_content_types = ["text/plain"] + + # Collections + enabled_rule_collection_ids = ["@builtin/crs/request"] + disabled_rule_collection_ids = [] + log_only_rule_collection_ids = ["@builtin/crs/response"] + + # Groups + enabled_rule_group_ids = [] + disabled_rule_group_ids = [] + log_only_rule_group_ids = [] + + # Specific Rules (Highest Precedence) + enabled_rule_ids = ["@builtin/crs/request/913100"] + disabled_rule_ids = ["@builtin/crs/request/942151"] + log_only_rule_ids = ["@builtin/crs/response/954120"] + } } } diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index af6c0518c..41cf7cd64 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -91,15 +91,15 @@ var schemaDescriptions = map[string]string{ "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`.", "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`.", "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`.", - "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", } type Model struct { @@ -478,10 +478,8 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Computed: true, Attributes: map[string]schema.Attribute{ "mode": schema.StringAttribute{ - Optional: true, - Computed: true, + Required: true, Description: schemaDescriptions["waf_mode"], - Default: stringdefault.StaticString("DISABLED"), }, "type": schema.StringAttribute{ Optional: true, From d3929ad07071d2b0cba3d31437c6cd270b36df01 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 13 Apr 2026 15:28:54 +0200 Subject: [PATCH 19/62] fyi: improve some validations and description --- docs/data-sources/cdn_distribution.md | 4 +-- docs/resources/cdn_distribution.md | 28 +++++++++---------- .../services/cdn/distribution/datasource.go | 2 +- .../services/cdn/distribution/resource.go | 11 +++++++- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index ac6ad8de1..255ebaf2f 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -27,7 +27,7 @@ data "stackit_cdn_distribution" "example" { ### Required -- `distribution_id` (String) STACKIT project ID associated with the distribution +- `distribution_id` (String) CDN distribution ID - `project_id` (String) STACKIT project ID associated with the distribution ### Read-Only @@ -61,7 +61,7 @@ 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) The configured type http to configure countries where content is allowed. A map of URLs to a list of countries +- `geofencing` (Map of List of String) Routes users from specific countries to alternative origins (HTTP backend required). Configure this by mapping the alternative origin URL to a list of country codes - `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'. diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index 72f3ba49d..5de2f41ab 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -75,7 +75,7 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { ] } -# WAF Configuration + # WAF Configuration # # Precedence Hierarchy: Specific Rules > Groups > Collections # In this example, the entire "@builtin/crs/request" collection is ENABLED. @@ -91,21 +91,21 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { allowed_http_versions = ["HTTP/1.0", "HTTP/1.1"] allowed_http_methods = ["GET"] allowed_request_content_types = ["text/plain"] - + # Collections - enabled_rule_collection_ids = ["@builtin/crs/request"] - disabled_rule_collection_ids = [] - log_only_rule_collection_ids = ["@builtin/crs/response"] - + enabled_rule_collection_ids = ["@builtin/crs/request"] + disabled_rule_collection_ids = [] + log_only_rule_collection_ids = ["@builtin/crs/response"] + # Groups - enabled_rule_group_ids = [] - disabled_rule_group_ids = [] - log_only_rule_group_ids = [] - + enabled_rule_group_ids = [] + disabled_rule_group_ids = [] + log_only_rule_group_ids = [] + # Specific Rules (Highest Precedence) - enabled_rule_ids = ["@builtin/crs/request/913100"] - disabled_rule_ids = ["@builtin/crs/request/942151"] - log_only_rule_ids = ["@builtin/crs/response/954120"] + enabled_rule_ids = ["@builtin/crs/request/913100"] + disabled_rule_ids = ["@builtin/crs/request/942151"] + log_only_rule_ids = ["@builtin/crs/response/954120"] } } } @@ -161,7 +161,7 @@ Optional: - `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 +- `geofencing` (Map of List of String) Routes users from specific countries to alternative origins (HTTP backend required). Configure this by mapping the alternative origin URL to a list of country codes - `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'. diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index cfe5598b5..7a86f342f 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -93,7 +93,7 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe Computed: true, }, "distribution_id": schema.StringAttribute{ - Description: schemaDescriptions["project_id"], + Description: schemaDescriptions["distribution_id"], Required: true, Validators: []validator.String{ validate.UUID(), diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 41cf7cd64..01c2f8052 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -64,7 +64,7 @@ var schemaDescriptions = map[string]string{ "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_backend_geofencing": "Routes users from specific countries to alternative origins (HTTP backend required). Configure this by mapping the alternative origin URL to a list of country codes", "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", @@ -499,6 +499,9 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_versions"], Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedHttpVersions)), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, }, "allowed_request_content_types": schema.ListAttribute{ Optional: true, @@ -506,6 +509,9 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_request_content_types"], Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedRequestContentTypes)), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, }, "allowed_http_methods": schema.ListAttribute{ Optional: true, @@ -513,6 +519,9 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_methods"], Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedHttpMethods)), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, }, "enabled_rule_ids": schema.ListAttribute{ Optional: true, From 100d9f6c2cc0725ff484ffde4a10029ca8c7e4b1 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 13 Apr 2026 15:49:56 +0200 Subject: [PATCH 20/62] fyi: add checks in acc_test --- stackit/internal/services/cdn/cdn_acc_test.go | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 644833358..9281dc693 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -103,6 +103,11 @@ func configVarsHttpUpdated() config.Variables { updatedConfig := maps.Clone(testConfigVarsHttp) updatedConfig["regions"] = config.ListVariable(config.StringVariable("EU"), config.StringVariable("US"), config.StringVariable("ASIA")) updatedConfig["redirect_target_url"] = config.StringVariable("https://example.com/updated") + + // Update WAF configuration to test mutation + updatedConfig["waf_mode"] = config.StringVariable("ENABLED") + updatedConfig["waf_enabled_rule_ids"] = config.ListVariable(config.StringVariable("@builtin/crs/request/941120"), config.StringVariable("@builtin/crs/request/941130")) + return updatedConfig } @@ -183,6 +188,12 @@ func TestAccCDNDistributionHttp(t *testing.T) { "DE", ), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["optimizer"])), + // WAF Checks + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.mode", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_mode"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.type", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_type"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", "@builtin/crs/request/941120"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), ), @@ -285,6 +296,13 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.blocked_countries.#", "1"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.optimizer.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["optimizer"])), + + // WAF Checks inside Data Source + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.mode", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_mode"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.type", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_type"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", "@builtin/crs/request/941120"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.#", "1"), @@ -319,6 +337,14 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "1"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["optimizer"])), + + // Checking WAF Mutated Configurations + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.mode", "ENABLED"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.type", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_type"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.#", "2"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", "@builtin/crs/request/941120"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.1", "@builtin/crs/request/941130"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), resource.TestCheckResourceAttr( From 7f5e08d8211a5a19c1692d908760e3ec6f1da6ed Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 13 Apr 2026 16:21:13 +0200 Subject: [PATCH 21/62] FYI: add all field in acc_test --- stackit/internal/services/cdn/cdn_acc_test.go | 49 ++++++++++++------- .../cdn/testdata/resource-http-base.tf | 14 ++++-- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 293ef0962..0d8e52896 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -76,24 +76,28 @@ func configVarsBucketUpdated() config.Variables { } var testConfigVarsHttp = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(httpTestName), - "regions": config.ListVariable(config.StringVariable("EU"), config.StringVariable("US")), - "dns_zone_name": config.StringVariable("acc_cdn_test_zone"), - "dns_name": config.StringVariable(dnsNameHttp), - "dns_record_name": config.StringVariable(dnsRecordNameHttp), - "optimizer": config.BoolVariable(true), - "backend_http_type": config.StringVariable("http"), - "blocked_countries": config.ListVariable(config.StringVariable("CU")), - "backend_origin_url": config.StringVariable("https://test-backend-1.cdn-dev.runs.onstackit.cloud"), - "geofencing_list": config.ListVariable(config.StringVariable("DE")), - "origin_request_headers_name": config.StringVariable("X-Custom-Header"), - "origin_request_headers_value": config.StringVariable("x-custom-value"), - "certificate": config.StringVariable(string(cert)), - "private_key": config.StringVariable(string(key)), - "redirect_target_url": config.StringVariable("https://example.com"), - "redirect_status_code": config.IntegerVariable(301), - "redirect_matcher_value": config.StringVariable("/shop/*"), + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(httpTestName), + "regions": config.ListVariable(config.StringVariable("EU"), config.StringVariable("US")), + "dns_zone_name": config.StringVariable("acc_cdn_test_zone"), + "dns_name": config.StringVariable(dnsNameHttp), + "dns_record_name": config.StringVariable(dnsRecordNameHttp), + "optimizer": config.BoolVariable(true), + "backend_http_type": config.StringVariable("http"), + "blocked_countries": config.ListVariable(config.StringVariable("CU")), + "backend_origin_url": config.StringVariable("https://test-backend-1.cdn-dev.runs.onstackit.cloud"), + "geofencing_list": config.ListVariable(config.StringVariable("DE")), + "origin_request_headers_name": config.StringVariable("X-Custom-Header"), + "origin_request_headers_value": config.StringVariable("x-custom-value"), + "certificate": config.StringVariable(string(cert)), + "private_key": config.StringVariable(string(key)), + "redirect_target_url": config.StringVariable("https://example.com"), + "redirect_status_code": config.IntegerVariable(301), + "redirect_matcher_value": config.StringVariable("/shop/*"), + "redirect_rule_description": config.StringVariable("Acc test redirect"), + "redirect_rule_enabled": config.BoolVariable(true), + "redirect_rule_match_condition": config.StringVariable("ANY"), + "redirect_matcher_condition": config.StringVariable("ANY"), } func configVarsHttpUpdated() config.Variables { @@ -162,8 +166,12 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.#", "1"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.target_url", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_target_url"])), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.status_code", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_status_code"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.description", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_rule_description"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_rule_enabled"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.rule_match_condition", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_rule_match_condition"])), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.#", "1"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.0.values.0", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_matcher_value"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.0.value_match_condition", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_matcher_condition"])), 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"), @@ -287,6 +295,11 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.#", "1"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.target_url", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_target_url"])), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.status_code", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_status_code"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.description", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_rule_description"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_rule_enabled"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.rule_match_condition", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_rule_match_condition"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.0.values.0", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_matcher_value"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.0.matchers.0.value_match_condition", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_matcher_condition"])), resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", fullDomainNameHttp), diff --git a/stackit/internal/services/cdn/testdata/resource-http-base.tf b/stackit/internal/services/cdn/testdata/resource-http-base.tf index 026d1b990..9777ee95c 100644 --- a/stackit/internal/services/cdn/testdata/resource-http-base.tf +++ b/stackit/internal/services/cdn/testdata/resource-http-base.tf @@ -11,7 +11,11 @@ variable "certificate" {} variable "private_key" {} variable "redirect_target_url" {} variable "redirect_status_code" {} +variable "redirect_rule_description" {} +variable "redirect_rule_enabled" {} +variable "redirect_rule_match_condition" {} variable "redirect_matcher_value" {} +variable "redirect_matcher_condition" {} # dns variable "dns_zone_name" {} @@ -45,11 +49,15 @@ resource "stackit_cdn_distribution" "distribution" { redirects = { rules = [ { - target_url = var.redirect_target_url - status_code = var.redirect_status_code + description = var.redirect_rule_description + enabled = var.redirect_rule_enabled + target_url = var.redirect_target_url + status_code = var.redirect_status_code + rule_match_condition = var.redirect_rule_match_condition matchers = [ { - values = [var.redirect_matcher_value] + values = [var.redirect_matcher_value] + value_match_condition = var.redirect_matcher_condition } ] } From a41657a774ab22bf175659557388f9292b6472f6 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 23 Apr 2026 11:59:24 +0200 Subject: [PATCH 22/62] feat: DRY creatian redirect Config up --- .../services/cdn/distribution/resource.go | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 09982cb34..a7998d2cc 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -702,47 +702,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } // redirects - var redirectsConfig *cdnSdk.RedirectConfig - if configModel.Redirects != nil { - sdkRules := []cdnSdk.RedirectRule{} - if len(configModel.Redirects.Rules) > 0 { - for _, rule := range configModel.Redirects.Rules { - matchers := []cdnSdk.Matcher{} - for _, matcher := range rule.Matchers { - var matchCond *cdnSdk.MatchCondition - if matcher.ValueMatchCondition != nil { - cond := cdnSdk.MatchCondition(*matcher.ValueMatchCondition) - matchCond = &cond - } - - matchers = append(matchers, cdnSdk.Matcher{ - Values: matcher.Values, - ValueMatchCondition: matchCond, - }) - } - - var ruleMatchCond *cdnSdk.MatchCondition - if rule.RuleMatchCondition != nil { - cond := cdnSdk.MatchCondition(*rule.RuleMatchCondition) - ruleMatchCond = &cond - } - targetUrl := rule.TargetUrl - - sdkConfigRule := cdnSdk.RedirectRule{ - Description: rule.Description, - Enabled: rule.Enabled, - Matchers: matchers, - RuleMatchCondition: ruleMatchCond, - StatusCode: rule.StatusCode, - TargetUrl: targetUrl, - } - sdkRules = append(sdkRules, sdkConfigRule) - } - } - redirectsConfig = &cdnSdk.RedirectConfig{ - Rules: sdkRules, - } - } + redirectsConfig := convertRedirectconfig(configModel.Redirects) configPatchBackend := &cdnSdk.ConfigPatchBackend{} @@ -1284,6 +1244,52 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut return payload, nil } +func convertRedirectconfig(redirectConfigModel *redirectConfig) *cdnSdk.RedirectConfig { + var redirectsConfig *cdnSdk.RedirectConfig + if redirectConfigModel != nil { + sdkRules := []cdnSdk.RedirectRule{} + if len(redirectsConfig.Rules) > 0 { + for _, rule := range redirectsConfig.Rules { + matchers := []cdnSdk.Matcher{} + for _, matcher := range rule.Matchers { + var matchCond *cdnSdk.MatchCondition + if matcher.ValueMatchCondition != nil { + cond := cdnSdk.MatchCondition(*matcher.ValueMatchCondition) + matchCond = &cond + } + + matchers = append(matchers, cdnSdk.Matcher{ + Values: matcher.Values, + ValueMatchCondition: matchCond, + }) + } + + var ruleMatchCond *cdnSdk.MatchCondition + if rule.RuleMatchCondition != nil { + cond := cdnSdk.MatchCondition(*rule.RuleMatchCondition) + ruleMatchCond = &cond + } + targetUrl := rule.TargetUrl + + sdkConfigRule := cdnSdk.RedirectRule{ + Description: rule.Description, + Enabled: rule.Enabled, + Matchers: matchers, + RuleMatchCondition: ruleMatchCond, + StatusCode: rule.StatusCode, + TargetUrl: targetUrl, + } + sdkRules = append(sdkRules, sdkConfigRule) + } + } + redirectsConfig = &cdnSdk.RedirectConfig{ + Rules: sdkRules, + } + } + return redirectsConfig + +} + func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { if model == nil { return nil, errors.New("model cannot be nil") @@ -1324,7 +1330,7 @@ func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { } // redirects - var redirectsConfig *cdnSdk.RedirectConfig + redirectsConfig := convertRedirectconfig(configModel.Redirects) if configModel.Redirects != nil { sdkRules := []cdnSdk.RedirectRule{} From 06c4b5c336b64aa6c6449bf4f5937be3f11eea0a Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 23 Apr 2026 12:16:41 +0200 Subject: [PATCH 23/62] chore: use SDK match condition --- stackit/internal/services/cdn/distribution/resource.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index a7998d2cc..2cf88f997 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -30,6 +30,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" cdnSdk "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" @@ -245,7 +246,6 @@ 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), @@ -369,7 +369,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Computed: true, Description: schemaDescriptions["config_redirects_rule_match_condition"], Default: stringdefault.StaticString("ANY"), - Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(matchCondition...)}, + Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedMatchConditionEnumValues)...)}, }, "matchers": schema.ListNestedAttribute{ Description: schemaDescriptions["config_redirects_rule_matchers"], @@ -392,7 +392,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Description: schemaDescriptions["config_redirects_rule_match_condition"], Default: stringdefault.StaticString("ANY"), Computed: true, - Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(matchCondition...)}, + Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedMatchConditionEnumValues)...)}, }, }, }, From b756791481ad9e4af23abcd4b6b83691ce5458d5 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 23 Apr 2026 12:52:26 +0200 Subject: [PATCH 24/62] chore: some small tweaks to avoid redundancy --- .../services/cdn/distribution/resource.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 2cf88f997..e92c1da9c 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -912,13 +912,7 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo 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) + tfValuesList, diags := types.ListValueFrom(ctx, types.StringType, m.Values) if diags.HasError() { return core.DiagsToError(diags) } @@ -961,7 +955,7 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo tfStatusCode := types.Int32Null() if r.StatusCode > 0 { - tfStatusCode = types.Int32Value(int32(r.StatusCode)) // nolint:gosec // HTTP status codes are safely within int32 bounds + tfStatusCode = types.Int32Value(r.StatusCode) } tfRuleMatchCond := types.StringValue("ANY") @@ -1266,8 +1260,7 @@ func convertRedirectconfig(redirectConfigModel *redirectConfig) *cdnSdk.Redirect var ruleMatchCond *cdnSdk.MatchCondition if rule.RuleMatchCondition != nil { - cond := cdnSdk.MatchCondition(*rule.RuleMatchCondition) - ruleMatchCond = &cond + ruleMatchCond = new(cdnSdk.MatchCondition(*rule.RuleMatchCondition)) } targetUrl := rule.TargetUrl @@ -1353,8 +1346,7 @@ func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { var ruleMatchCond *cdnSdk.MatchCondition if rule.RuleMatchCondition != nil { - cond := cdnSdk.MatchCondition(*rule.RuleMatchCondition) - ruleMatchCond = &cond + ruleMatchCond = new(cdnSdk.MatchCondition(*rule.RuleMatchCondition)) } sdkConfigRule := cdnSdk.RedirectRule{ From 0fd82a082a6a6eab27d484be1b3f505761b38de8 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 23 Apr 2026 14:06:50 +0200 Subject: [PATCH 25/62] chore: some small tweaks in test --- .../services/cdn/distribution/resource_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index b6c64b20b..bbbbfe01a 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -165,11 +165,11 @@ func TestToCreatePayload(t *testing.T) { Enabled: cdnSdk.PtrBool(true), TargetUrl: "https://example.com/redirect", StatusCode: 301, - RuleMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + RuleMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), Matchers: []cdnSdk.Matcher{ { Values: []string{"/shop/*"}, - ValueMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + ValueMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), }, }, }, @@ -419,11 +419,11 @@ func TestConvertConfig(t *testing.T) { Enabled: cdnSdk.PtrBool(true), TargetUrl: "https://example.com/redirect", StatusCode: 301, - RuleMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + RuleMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), Matchers: []cdnSdk.Matcher{ { Values: []string{"/shop/*"}, - ValueMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + ValueMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), }, }, }, @@ -561,11 +561,11 @@ func TestMapFields(t *testing.T) { Enabled: cdnSdk.PtrBool(true), TargetUrl: "https://example.com/redirect", StatusCode: 301, - RuleMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + RuleMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), Matchers: []cdnSdk.Matcher{ { Values: []string{"/shop/*"}, - ValueMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + ValueMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), }, }, }, From 535a8d8084c4798adb06bccd4df9443d708287a0 Mon Sep 17 00:00:00 2001 From: Politano <47066134+matheuspolitano@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:09:16 +0200 Subject: [PATCH 26/62] chore: simplify the redirect set up in test Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- .../internal/services/cdn/distribution/resource_test.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index bbbbfe01a..b0c46c9fe 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -43,18 +43,12 @@ func TestToCreatePayload(t *testing.T) { "enabled": types.BoolValue(true), }) - redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) - if !ok { - t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") - } - redirectsAttrTypes := redirectsObjType.AttrTypes - config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) matcherValues := types.ListValueMust(types.StringType, []attr.Value{ From a1eda91dde757d08436fb5f2c20079d86c9fd9c1 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 23 Apr 2026 15:34:58 +0200 Subject: [PATCH 27/62] fix: use the right variable instead the new one, get rif of some redundant code --- .../services/cdn/distribution/resource.go | 4 ++-- .../services/cdn/distribution/resource_test.go | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index e92c1da9c..1bc42b77f 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1242,8 +1242,8 @@ func convertRedirectconfig(redirectConfigModel *redirectConfig) *cdnSdk.Redirect var redirectsConfig *cdnSdk.RedirectConfig if redirectConfigModel != nil { sdkRules := []cdnSdk.RedirectRule{} - if len(redirectsConfig.Rules) > 0 { - for _, rule := range redirectsConfig.Rules { + if len(redirectConfigModel.Rules) > 0 { + for _, rule := range redirectConfigModel.Rules { matchers := []cdnSdk.Matcher{} for _, matcher := range rule.Matchers { var matchCond *cdnSdk.MatchCondition diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index b0c46c9fe..f00fa7969 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -113,7 +113,7 @@ func TestToCreatePayload(t *testing.T) { "regions": regionsFixture, "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) }), Expected: &cdnSdk.CreateDistributionPayload{ @@ -192,7 +192,7 @@ func TestToCreatePayload(t *testing.T) { "regions": regionsFixture, // reusing the existing one "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) }), Expected: &cdnSdk.CreateDistributionPayload{ @@ -275,18 +275,12 @@ func TestConvertConfig(t *testing.T) { blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{"enabled": types.BoolValue(true)}) - redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) - if !ok { - t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") - } - redirectsAttrTypes := redirectsObjType.AttrTypes - config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, "regions": regionsFixture, "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) matcherValues := types.ListValueMust(types.StringType, []attr.Value{ @@ -357,7 +351,7 @@ func TestConvertConfig(t *testing.T) { "regions": regionsFixture, "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) }), Expected: &cdnSdk.Config{ @@ -446,7 +440,7 @@ func TestConvertConfig(t *testing.T) { "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) }), Expected: &cdnSdk.Config{ From 633ac4f9545759a1f4c1c3fbd2e11ec1c326dce6 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 23 Apr 2026 16:10:38 +0200 Subject: [PATCH 28/62] chore: remove redundant string in acc_test --- stackit/internal/services/cdn/cdn_acc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 0d8e52896..562c21767 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -343,7 +343,7 @@ func TestAccCDNDistributionHttp(t *testing.T) { ), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.#", "1"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.target_url", "https://example.com/updated"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.target_url", testutil.ConvertConfigVariable(configVarsHttpUpdated()["redirect_target_url"])), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.redirects.rules.0.status_code", testutil.ConvertConfigVariable(testConfigVarsHttp["redirect_status_code"])), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), From 4cc4b60d4afd85426476c0689abd50e168496a4e Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 23 Apr 2026 16:49:32 +0200 Subject: [PATCH 29/62] chore: remove empty line at the end of the function and order acc import by name --- stackit/internal/services/cdn/cdn_acc_test.go | 4 +++- stackit/internal/services/cdn/distribution/resource.go | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 562c21767..f7be85668 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -21,10 +21,12 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - cdnSdk "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" + + cdnSdk "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" ) var ( diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 18c52987f..71cf46332 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1281,7 +1281,6 @@ func convertRedirectconfig(redirectConfigModel *redirectConfig) *cdnSdk.Redirect } } return redirectsConfig - } func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { From 43ad0302ec1f5288adc6341a910b27218eb3b704 Mon Sep 17 00:00:00 2001 From: Politano <47066134+matheuspolitano@users.noreply.github.com> Date: Fri, 24 Apr 2026 09:50:50 +0200 Subject: [PATCH 30/62] feat: remove the case insensitive Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- stackit/internal/services/cdn/distribution/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 71cf46332..2f35ab0a2 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -393,7 +393,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Description: schemaDescriptions["config_redirects_rule_match_condition"], Default: stringdefault.StaticString("ANY"), Computed: true, - Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedMatchConditionEnumValues)...)}, + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedMatchConditionEnumValues)...)}, }, }, }, From e1ee94f23464964b49fb1ea1b37dc35fbb65c0ac Mon Sep 17 00:00:00 2001 From: Politano <47066134+matheuspolitano@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:20:54 +0200 Subject: [PATCH 31/62] feat: add validation to no null values Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- stackit/internal/services/cdn/distribution/resource.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 2f35ab0a2..cd79e43cc 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -386,6 +386,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques ElementType: types.StringType, Validators: []validator.List{ listvalidator.SizeAtLeast(1), + listvalidator.NoNullValues(), }, }, "value_match_condition": schema.StringAttribute{ From be8a32ef78b7758793b69b32b2b5316d5ba1e3ed Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 24 Apr 2026 10:23:45 +0200 Subject: [PATCH 32/62] chore: remove redundant code --- .../services/cdn/distribution/datasource_test.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/datasource_test.go b/stackit/internal/services/cdn/distribution/datasource_test.go index df8440f4b..b5170e38f 100644 --- a/stackit/internal/services/cdn/distribution/datasource_test.go +++ b/stackit/internal/services/cdn/distribution/datasource_test.go @@ -8,7 +8,6 @@ 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" cdnSdk "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" ) @@ -40,18 +39,12 @@ func TestMapDataSourceFields(t *testing.T) { optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(true), }) - // Safely assert the type - redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) - if !ok { - t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") - } - redirectsAttrTypes := redirectsObjType.AttrTypes config := types.ObjectValueMust(dataSourceConfigTypes, map[string]attr.Value{ "backend": backend, "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) redirectsInput := cdnSdk.RedirectConfig{ Rules: []cdnSdk.RedirectRule{ From 0d3507a785dc61389e2d22bcf96c781f525cde10 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 24 Apr 2026 10:29:38 +0200 Subject: [PATCH 33/62] fix: add redirecttype --- .../internal/services/cdn/distribution/datasource_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/datasource_test.go b/stackit/internal/services/cdn/distribution/datasource_test.go index b5170e38f..5e2e811c4 100644 --- a/stackit/internal/services/cdn/distribution/datasource_test.go +++ b/stackit/internal/services/cdn/distribution/datasource_test.go @@ -171,7 +171,7 @@ func TestMapDataSourceFields(t *testing.T) { "regions": regionsFixture, "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -197,7 +197,7 @@ func TestMapDataSourceFields(t *testing.T) { "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) }), IsValid: true, @@ -217,7 +217,7 @@ func TestMapDataSourceFields(t *testing.T) { "regions": regionsFixture, "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, - "redirects": types.ObjectNull(redirectsAttrTypes), + "redirects": types.ObjectNull(redirectsTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { From 6c1b069c4e17c482165d9cd226026cf30cf82aa2 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 24 Apr 2026 10:32:56 +0200 Subject: [PATCH 34/62] chore: change the all matcho dondition to all --- .../cdn/distribution/resource_test.go | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index f00fa7969..ed3de6f71 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -56,7 +56,7 @@ func TestToCreatePayload(t *testing.T) { }) matcherVal := types.ObjectValueMust(matcherTypes, map[string]attr.Value{ "values": matcherValues, - "value_match_condition": types.StringValue("ANY"), + "value_match_condition": types.StringValue("ALL"), }) matchersList := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherVal}) @@ -65,7 +65,7 @@ func TestToCreatePayload(t *testing.T) { "enabled": types.BoolValue(true), "target_url": types.StringValue("https://example.com/redirect"), "status_code": types.Int32Value(301), - "rule_match_condition": types.StringValue("ANY"), + "rule_match_condition": types.StringValue("ALL"), "matchers": matchersList, }) rulesList := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleVal}) @@ -159,11 +159,11 @@ func TestToCreatePayload(t *testing.T) { Enabled: cdnSdk.PtrBool(true), TargetUrl: "https://example.com/redirect", StatusCode: 301, - RuleMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), + RuleMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), Matchers: []cdnSdk.Matcher{ { Values: []string{"/shop/*"}, - ValueMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), + ValueMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), }, }, }, @@ -288,7 +288,7 @@ func TestConvertConfig(t *testing.T) { }) matcherVal := types.ObjectValueMust(matcherTypes, map[string]attr.Value{ "values": matcherValues, - "value_match_condition": types.StringValue("ANY"), + "value_match_condition": types.StringValue("ALL"), }) matchersList := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherVal}) @@ -297,7 +297,7 @@ func TestConvertConfig(t *testing.T) { "enabled": types.BoolValue(true), "target_url": types.StringValue("https://example.com/redirect"), "status_code": types.Int32Value(301), - "rule_match_condition": types.StringValue("ANY"), + "rule_match_condition": types.StringValue("ALL"), "matchers": matchersList, }) rulesList := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleVal}) @@ -411,7 +411,7 @@ func TestConvertConfig(t *testing.T) { Matchers: []cdnSdk.Matcher{ { Values: []string{"/shop/*"}, - ValueMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), + ValueMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), }, }, }, @@ -549,11 +549,11 @@ func TestMapFields(t *testing.T) { Enabled: cdnSdk.PtrBool(true), TargetUrl: "https://example.com/redirect", StatusCode: 301, - RuleMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), + RuleMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), Matchers: []cdnSdk.Matcher{ { Values: []string{"/shop/*"}, - ValueMatchCondition: cdnSdk.MATCHCONDITION_ANY.Ptr(), + ValueMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), }, }, }, @@ -565,7 +565,7 @@ func TestMapFields(t *testing.T) { }) matcherValExpected := types.ObjectValueMust(matcherTypes, map[string]attr.Value{ "values": matcherValuesExpected, - "value_match_condition": types.StringValue("ANY"), + "value_match_condition": types.StringValue("ALL"), }) matchersListExpected := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherValExpected}) @@ -574,7 +574,7 @@ func TestMapFields(t *testing.T) { "enabled": types.BoolValue(true), "target_url": types.StringValue("https://example.com/redirect"), "status_code": types.Int32Value(301), - "rule_match_condition": types.StringValue("ANY"), + "rule_match_condition": types.StringValue("ALL"), "matchers": matchersListExpected, }) rulesListExpected := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleValExpected}) From e93dcb2989e947a4f647614134341c2c15bca8b8 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 24 Apr 2026 11:15:35 +0200 Subject: [PATCH 35/62] feat: make match condition sensitive --- stackit/internal/services/cdn/distribution/resource.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index cd79e43cc..9c51a3d83 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -370,7 +370,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Computed: true, Description: schemaDescriptions["config_redirects_rule_match_condition"], Default: stringdefault.StaticString("ANY"), - Validators: []validator.String{stringvalidator.OneOfCaseInsensitive(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedMatchConditionEnumValues)...)}, + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedMatchConditionEnumValues)...)}, }, "matchers": schema.ListNestedAttribute{ Description: schemaDescriptions["config_redirects_rule_matchers"], From e8d4324df4609fe1ce36682fe9ffa91e3dcd838c Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 24 Apr 2026 11:38:47 +0200 Subject: [PATCH 36/62] chore: remove portuguese comment --- stackit/internal/services/cdn/distribution/resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index ed3de6f71..1501f872f 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -381,7 +381,7 @@ func TestConvertConfig(t *testing.T) { "regions": regionsFixture, "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, - "redirects": redirectsConfigVal, // Injetando o mock aqui + "redirects": redirectsConfigVal, }) }), Expected: &cdnSdk.Config{ From d8448a1d05a8d4db909fcea96a7808a07fde643e Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Fri, 24 Apr 2026 11:44:11 +0200 Subject: [PATCH 37/62] chore: switch to all ALL --- .../internal/services/cdn/distribution/datasource_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/datasource_test.go b/stackit/internal/services/cdn/distribution/datasource_test.go index 5e2e811c4..208383d4d 100644 --- a/stackit/internal/services/cdn/distribution/datasource_test.go +++ b/stackit/internal/services/cdn/distribution/datasource_test.go @@ -53,11 +53,11 @@ func TestMapDataSourceFields(t *testing.T) { Enabled: cdnSdk.PtrBool(true), TargetUrl: "https://example.com/redirect", StatusCode: 301, - RuleMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + RuleMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), Matchers: []cdnSdk.Matcher{ { Values: []string{"/shop/*"}, - ValueMatchCondition: cdnSdk.MatchCondition("ANY").Ptr(), + ValueMatchCondition: cdnSdk.MATCHCONDITION_ALL.Ptr(), }, }, }, @@ -68,7 +68,7 @@ func TestMapDataSourceFields(t *testing.T) { }) matcherValExpected := types.ObjectValueMust(matcherTypes, map[string]attr.Value{ "values": matcherValuesExpected, - "value_match_condition": types.StringValue("ANY"), + "value_match_condition": types.StringValue("ALL"), }) matchersListExpected := types.ListValueMust(types.ObjectType{AttrTypes: matcherTypes}, []attr.Value{matcherValExpected}) @@ -77,7 +77,7 @@ func TestMapDataSourceFields(t *testing.T) { "enabled": types.BoolValue(true), "target_url": types.StringValue("https://example.com/redirect"), "status_code": types.Int32Value(301), - "rule_match_condition": types.StringValue("ANY"), + "rule_match_condition": types.StringValue("ALL"), "matchers": matchersListExpected, }) rulesListExpected := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleValExpected}) From 7d99d219ddaf5e82e1fa8eb3518a6158c9c04a7c Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 29 Apr 2026 17:43:46 +0200 Subject: [PATCH 38/62] chore: create SortedStringToListVale in conversion --- stackit/internal/conversion/conversion.go | 15 ++++ .../internal/conversion/conversion_test.go | 68 +++++++++++++++++++ .../services/cdn/distribution/datasource.go | 25 ++++--- .../services/cdn/distribution/resource.go | 39 ++++------- 4 files changed, 107 insertions(+), 40 deletions(-) diff --git a/stackit/internal/conversion/conversion.go b/stackit/internal/conversion/conversion.go index 18cb69012..f1f404fad 100644 --- a/stackit/internal/conversion/conversion.go +++ b/stackit/internal/conversion/conversion.go @@ -263,3 +263,18 @@ func ParseEphemeralProviderData(ctx context.Context, providerData any, diags *di } return stackitProviderData, true } + +// SortedStringsToListValue guarantees the returned HCL List is sorted +func SortedStringsToListValue(items []string) basetypes.ListValue { + if len(items) == 0 { + return types.ListValueMust(types.StringType, []attr.Value{}) + } + sorted := make([]string, len(items)) + copy(sorted, items) + sort.Strings(sorted) + elements := make([]attr.Value, len(sorted)) + for i, val := range sorted { + elements[i] = types.StringValue(val) + } + return types.ListValueMust(types.StringType, elements) +} diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index 0ebfccf86..aa34a957b 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -511,3 +511,71 @@ func TestStringListToSlice(t *testing.T) { }) } } + +func TestSortedStringsToListValue(t *testing.T) { + tests := []struct { + name string + items []string + expected basetypes.ListValue + }{ + { + name: "empty slice", + items: []string{}, + expected: types.ListValueMust(types.StringType, []attr.Value{}), + }, + { + name: "nil slice", + items: nil, + expected: types.ListValueMust(types.StringType, []attr.Value{}), + }, + { + name: "unsorted slice", + items: []string{"c", "a", "b"}, + expected: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("a"), + types.StringValue("b"), + types.StringValue("c"), + }), + }, + { + name: "already sorted slice", + items: []string{"a", "b", "c"}, + expected: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("a"), + types.StringValue("b"), + types.StringValue("c"), + }), + }, + { + name: "with duplicates", + items: []string{"b", "a", "b"}, + expected: types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("a"), + types.StringValue("b"), + types.StringValue("b"), + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Keep a copy of original to ensure it's not mutated by the function + var original []string + if tt.items != nil { + original = make([]string, len(tt.items)) + copy(original, tt.items) + } + + actual := SortedStringsToListValue(tt.items) + + if !actual.Equal(tt.expected) { + t.Errorf("expected %v, got %v", tt.expected, actual) + } + + // Verify original slice was not mutated + if !reflect.DeepEqual(original, tt.items) { + t.Errorf("original slice was mutated: expected %v, got %v", original, tt.items) + } + }) + } +} diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index 4c60dadd6..a23e03777 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -607,19 +607,18 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, wafObjAttrs["paranoia_level"] = types.StringNull() } - // Uses the mapWafListToHCL defined in resource.go - wafObjAttrs["allowed_http_versions"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafListToHCL(distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleCollectionIds) var diagWaf diag.Diagnostics wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 701afd555..f766c2117 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1381,18 +1381,18 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo wafObjAttrs["paranoia_level"] = types.StringNull() } - wafObjAttrs["allowed_http_versions"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafListToHCL(distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafListToHCL(distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafListToHCL(distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleCollectionIds) // Safely determine if the API considers the WAF completely disabled isWafDisabled := distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED && @@ -1803,21 +1803,6 @@ func getSortedWafList(ctx context.Context, tfList basetypes.ListValue) []string return elements } -// mapWafListToHCL guarantees the returned HCL List is sorted -func mapWafListToHCL(apiList []string) basetypes.ListValue { - if len(apiList) == 0 { - return types.ListValueMust(types.StringType, []attr.Value{}) - } - sorted := make([]string, len(apiList)) - copy(sorted, apiList) - sort.Strings(sorted) - var elements []attr.Value - for _, val := range sorted { - elements = append(elements, types.StringValue(val)) - } - return types.ListValueMust(types.StringType, elements) -} - // sortedStringListToAttrValueList sorts a slice of strings and converts it // to a slice of attr.Value for use in Terraform schema defaults. func sortedStringListToAttrValueList(items []string) []attr.Value { From 3c4cf9573f426c13d558348099105313484a464f Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 29 Apr 2026 17:58:47 +0200 Subject: [PATCH 39/62] chore: improve the documentations --- .../services/cdn/distribution/resource.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index f766c2117..83f40ff67 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -93,15 +93,15 @@ var schemaDescriptions = map[string]string{ "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`.", "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`.", "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`.", - "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", } type Model struct { From c3c37c98cc524c77609f4b5fc2b5e4007f4101b9 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 30 Apr 2026 10:17:03 +0200 Subject: [PATCH 40/62] chore: improve cdn waf documentation --- docs/data-sources/cdn_distribution.md | 20 ++++++++++---------- docs/resources/cdn_distribution.md | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 255ebaf2f..281e4f5ce 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -61,7 +61,7 @@ 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) Routes users from specific countries to alternative origins (HTTP backend required). Configure this by mapping the alternative origin URL to a list of country codes +- `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'. @@ -114,15 +114,15 @@ Read-Only: - `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. - `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. - `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. - `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index 5de2f41ab..7e9c3f1a1 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -161,7 +161,7 @@ Optional: - `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) Routes users from specific countries to alternative origins (HTTP backend required). Configure this by mapping the alternative origin URL to a list of country codes +- `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'. @@ -232,15 +232,15 @@ Optional: - `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. - `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. - `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups, and Groups override Collections. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. - `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. From 72ddf6c6b2e7cab54970d2c004149b4b206ef36b Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Thu, 30 Apr 2026 14:48:48 +0200 Subject: [PATCH 41/62] chore: cdn remove the default value --- .../services/cdn/distribution/resource.go | 43 ++----------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 83f40ff67..d0a376e80 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -305,20 +305,6 @@ func (r *distributionResource) Metadata(_ context.Context, req resource.Metadata func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { backendOptions := []string{"http", "bucket"} statusCode := []int32{301, 302, 303, 307, 308} - defaultWafConfigAllowedHttpVersions := sortedStringListToAttrValueList([]string{ - "HTTP/1.0", "HTTP/1.1", "HTTP/2", "HTTP/2.0", - }) - defaultWafConfigAllowedRequestContentTypes := sortedStringListToAttrValueList([]string{ - "application/x-www-form-urlencoded", "multipart/form-data", "multipart/related", - "text/xml", "application/xml", "application/soap+xml", "application/x-amf", - "application/json", "application/octet-stream", "application/csp-report", - "application/xss-auditor-report", "text/plain", - }) - defaultWafConfigAllowedHttpMethods := sortedStringListToAttrValueList([]string{ - "GET", "HEAD", "POST", "PUT", "DELETE", - "CONNECT", "OPTIONS", "TRACE", "PATCH", - }) - resp.Schema = schema.Schema{ MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource), Description: "CDN distribution data source schema.", @@ -497,96 +483,73 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, "allowed_http_versions": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_versions"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedHttpVersions)), Validators: []validator.List{ listvalidator.SizeAtLeast(1), }, }, "allowed_request_content_types": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_request_content_types"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedRequestContentTypes)), Validators: []validator.List{ listvalidator.SizeAtLeast(1), }, }, "allowed_http_methods": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_methods"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, defaultWafConfigAllowedHttpMethods)), Validators: []validator.List{ listvalidator.SizeAtLeast(1), }, }, "enabled_rule_ids": schema.ListAttribute{ - Optional: true, - Computed: true, + Optional: true, + ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "disabled_rule_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "log_only_rule_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "enabled_rule_group_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_group_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "disabled_rule_group_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_group_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "log_only_rule_group_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_group_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "enabled_rule_collection_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_collection_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "disabled_rule_collection_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_collection_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, "log_only_rule_collection_ids": schema.ListAttribute{ Optional: true, - Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_collection_ids"], - Default: listdefault.StaticValue(types.ListValueMust(types.StringType, []attr.Value{})), }, }, }, @@ -1791,7 +1754,7 @@ func validateCountryCode(country string) (string, error) { // getSortedWafList extracts strings from HCL list, sorts them and returns the slice func getSortedWafList(ctx context.Context, tfList basetypes.ListValue) []string { - if tfList.IsNull() || tfList.IsUnknown() { + if utils.IsUndefined(tfList) { return nil } var elements []string From 1fdf69fcec2836232714dfab12f2fa9774b77c21 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 4 May 2026 12:27:45 +0200 Subject: [PATCH 42/62] chore: improve waf --- stackit/internal/services/cdn/cdn_acc_test.go | 11 +- .../services/cdn/distribution/resource.go | 141 ++++++++++-------- 2 files changed, 88 insertions(+), 64 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 13014dcdb..3e052ab84 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -245,9 +245,14 @@ func TestAccCDNDistributionHttp(t *testing.T) { return fmt.Sprintf("%s,%s", testutil.ProjectId, distributionId), nil }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"domains"}, // we added a domain in the meantime... + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domains", + "config.waf.allowed_http_methods", + "config.waf.allowed_http_versions", + "config.waf.allowed_request_content_types", + "config.waf.paranoia_level", + }, }, { ResourceName: "stackit_cdn_custom_domain.custom_domain", diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index d0a376e80..1614ff9ed 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -468,18 +468,17 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques "mode": schema.StringAttribute{ Required: true, Description: schemaDescriptions["waf_mode"], + Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFMODE_DISABLED), string(cdnSdk.WAFMODE_ENABLED), string(cdnSdk.WAFMODE_LOG_ONLY))}, }, "type": schema.StringAttribute{ - Optional: true, - Computed: true, + Required: true, Description: schemaDescriptions["waf_type"], - Default: stringdefault.StaticString("FREE"), + Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFTYPE_PREMIUM), string(cdnSdk.WAFTYPE_FREE))}, }, "paranoia_level": schema.StringAttribute{ Optional: true, - Computed: true, Description: schemaDescriptions["waf_paranoia_level"], - Default: stringdefault.StaticString("L1"), + Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFPARANOIALEVEL_L1), string(cdnSdk.WAFPARANOIALEVEL_L2), string(cdnSdk.WAFPARANOIALEVEL_L3), string(cdnSdk.WAFPARANOIALEVEL_L4))}, }, "allowed_http_versions": schema.ListAttribute{ Optional: true, @@ -954,22 +953,27 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe // User explicitly removed the WAF block from their terraform configuration modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED) typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE) - + var wafModel wafConfig + diags := configStateModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") + return + } wafPatch := cdnSdk.WafConfigPatch{ - Mode: &modeDisabled, - Type: &typeFree, - // Send empty arrays to clear rules, keeping the API happy - EnabledRuleIds: []string{}, - DisabledRuleIds: []string{}, - LogOnlyRuleIds: []string{}, - EnabledRuleGroupIds: []string{}, - DisabledRuleGroupIds: []string{}, - LogOnlyRuleGroupIds: []string{}, - EnabledRuleCollectionIds: []string{}, - DisabledRuleCollectionIds: []string{}, - LogOnlyRuleCollectionIds: []string{}, - // Intentionally omitted (nil) to avoid the 422 Unprocessable Entity error: - // AllowedHttpVersions, AllowedRequestContentTypes, AllowedHttpMethods + Mode: &modeDisabled, + Type: &typeFree, + AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions), + AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes), + AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods), + EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds), + DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds), + LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds), + EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds), + DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds), + LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds), + EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds), + DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds), + LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds), } configPatch.Waf = &wafPatch } @@ -1338,33 +1342,64 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo "type": types.StringValue(string(distribution.Config.Waf.Type)), } + // Detect if we are running an Import (or Data Source Read) where prior config doesn't exist + isImport := utils.IsUndefined(model.Config) + + // Parse old WAF state to respect omitted configurations during normal applies + var oldWaf wafConfig + if !isImport { + _ = oldConfig.Waf.As(ctx, &oldWaf, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: false, + UnhandledUnknownAsEmpty: false, + }) + } + + // Helper to conditionally map string fields + mapWafString := func(apiVal *string, oldVal types.String) types.String { + // Always map the API value during an Import, OR if the user explicitly defined it in HCL + if isImport || !oldVal.IsNull() { + if apiVal != nil { + return types.StringValue(*apiVal) + } + } + return types.StringNull() + } + + // Helper to conditionally map list fields + mapWafList := func(apiList []string, oldList types.List) types.List { + // Always map the API value during an Import, OR if the user explicitly defined it in HCL + if isImport || !oldList.IsNull() { + return conversion.SortedStringsToListValue(apiList) + } + return types.ListNull(types.StringType) + } + + var pl *string if distribution.Config.Waf.ParanoiaLevel != nil { - wafObjAttrs["paranoia_level"] = types.StringValue(string(*distribution.Config.Waf.ParanoiaLevel)) - } else { - wafObjAttrs["paranoia_level"] = types.StringNull() - } - - wafObjAttrs["allowed_http_versions"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleCollectionIds) - - // Safely determine if the API considers the WAF completely disabled - isWafDisabled := distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED && - distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE - - // If the WAF is disabled in the API, and there wasn't a WAF block in the user's previous state, - // keep it null to prevent state drift from residual backend default values. + plVal := string(*distribution.Config.Waf.ParanoiaLevel) + pl = &plVal + } + wafObjAttrs["paranoia_level"] = mapWafString(pl, oldWaf.ParanoiaLevel) + wafObjAttrs["allowed_http_versions"] = mapWafList(distribution.Config.Waf.AllowedHttpVersions, oldWaf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafList(distribution.Config.Waf.AllowedRequestContentTypes, oldWaf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafList(distribution.Config.Waf.AllowedHttpMethods, oldWaf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleIds, oldWaf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleIds, oldWaf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleIds, oldWaf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleGroupIds, oldWaf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleGroupIds, oldWaf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleGroupIds, oldWaf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleCollectionIds, oldWaf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleCollectionIds, oldWaf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleCollectionIds, oldWaf.LogOnlyRuleCollectionIds) + + // Determine if WAF should be entirely excluded to prevent drift. + // The API can return an empty string for fully unconfigured backends. + isWafDisabled := (distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED || distribution.Config.Waf.Mode == "") && + (distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE || distribution.Config.Waf.Type == "") + var wafVal attr.Value - if isWafDisabled && utils.IsUndefined(oldConfig.Waf) { + if isWafDisabled && (isImport || utils.IsUndefined(oldConfig.Waf)) { wafVal = types.ObjectNull(wafTypes) } else { var diagWaf diag.Diagnostics @@ -1765,19 +1800,3 @@ func getSortedWafList(ctx context.Context, tfList basetypes.ListValue) []string sort.Strings(elements) return elements } - -// sortedStringListToAttrValueList sorts a slice of strings and converts it -// to a slice of attr.Value for use in Terraform schema defaults. -func sortedStringListToAttrValueList(items []string) []attr.Value { - sortedItems := make([]string, len(items)) - copy(sortedItems, items) - - sort.Strings(sortedItems) - - attrValues := make([]attr.Value, len(sortedItems)) - for i, val := range sortedItems { - attrValues[i] = types.StringValue(val) - } - - return attrValues -} From 9dc726ce9ef4766a5c12ec1e5eb08b6a5fbed3a3 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 4 May 2026 15:45:59 +0200 Subject: [PATCH 43/62] chore: update docs --- .../services/cdn/distribution/resource.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 1614ff9ed..d60b0194c 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -93,15 +93,15 @@ var schemaDescriptions = map[string]string{ "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`.", "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`.", "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`.", - "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", } type Model struct { From 4b3a3128df2319a4bf02d086b5338faaa739890c Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Mon, 4 May 2026 16:42:32 +0200 Subject: [PATCH 44/62] chore: update doc --- docs/data-sources/cdn_distribution.md | 18 +++++++++--------- docs/resources/cdn_distribution.md | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 281e4f5ce..4ae8a4a76 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -114,15 +114,15 @@ Read-Only: - `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. - `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. - `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. - `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index 7e9c3f1a1..a772e5e97 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -226,23 +226,23 @@ Optional: Required: - `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. +- `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. Optional: - `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. - `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. - `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. -- `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. From 578a3a5345848db20b4e4c34e31a6f6109bdaebe Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 09:05:50 +0200 Subject: [PATCH 45/62] chore: small changes to improve the code quality --- docs/resources/cdn_distribution.md | 2 +- .../stackit_cdn_distribution/resource.tf | 2 +- stackit/internal/conversion/conversion.go | 3 + .../internal/conversion/conversion_test.go | 2 +- .../services/cdn/distribution/resource.go | 112 ++++++------------ 5 files changed, 43 insertions(+), 78 deletions(-) diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index a772e5e97..b865040f6 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -83,7 +83,7 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { # "@builtin/crs/request/942151" is explicitly DISABLED, overriding the collection setting. # # To view all available collections, groups, and rules, consult the API documentation: - # https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections + # https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections. waf = { mode = "ENABLED" type = "PREMIUM" diff --git a/examples/resources/stackit_cdn_distribution/resource.tf b/examples/resources/stackit_cdn_distribution/resource.tf index c532b0ee0..afeabecc6 100644 --- a/examples/resources/stackit_cdn_distribution/resource.tf +++ b/examples/resources/stackit_cdn_distribution/resource.tf @@ -65,7 +65,7 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { # "@builtin/crs/request/942151" is explicitly DISABLED, overriding the collection setting. # # To view all available collections, groups, and rules, consult the API documentation: - # https://internal-docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections + # https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections waf = { mode = "ENABLED" type = "PREMIUM" diff --git a/stackit/internal/conversion/conversion.go b/stackit/internal/conversion/conversion.go index f1f404fad..936e5a591 100644 --- a/stackit/internal/conversion/conversion.go +++ b/stackit/internal/conversion/conversion.go @@ -266,6 +266,9 @@ func ParseEphemeralProviderData(ctx context.Context, providerData any, diags *di // SortedStringsToListValue guarantees the returned HCL List is sorted func SortedStringsToListValue(items []string) basetypes.ListValue { + if items == nil { + return types.ListNull(types.StringType) + } if len(items) == 0 { return types.ListValueMust(types.StringType, []attr.Value{}) } diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index aa34a957b..b48425ad3 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -526,7 +526,7 @@ func TestSortedStringsToListValue(t *testing.T) { { name: "nil slice", items: nil, - expected: types.ListValueMust(types.StringType, []attr.Value{}), + expected: types.ListNull(types.StringType), }, { name: "unsorted slice", diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index d60b0194c..a1d6c69b4 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -468,17 +468,17 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques "mode": schema.StringAttribute{ Required: true, Description: schemaDescriptions["waf_mode"], - Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFMODE_DISABLED), string(cdnSdk.WAFMODE_ENABLED), string(cdnSdk.WAFMODE_LOG_ONLY))}, + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafModeEnumValues)...)}, }, "type": schema.StringAttribute{ Required: true, Description: schemaDescriptions["waf_type"], - Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFTYPE_PREMIUM), string(cdnSdk.WAFTYPE_FREE))}, + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafTypeEnumValues)...)}, }, "paranoia_level": schema.StringAttribute{ Optional: true, Description: schemaDescriptions["waf_paranoia_level"], - Validators: []validator.String{stringvalidator.OneOf(string(cdnSdk.WAFPARANOIALEVEL_L1), string(cdnSdk.WAFPARANOIALEVEL_L2), string(cdnSdk.WAFPARANOIALEVEL_L3), string(cdnSdk.WAFPARANOIALEVEL_L4))}, + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafParanoiaLevelEnumValues)...)}, }, "allowed_http_versions": schema.ListAttribute{ Optional: true, @@ -803,15 +803,8 @@ func (r *distributionResource) Read(ctx context.Context, req resource.ReadReques } func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var planModel Model - diags := req.Plan.Get(ctx, &planModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var stateModel Model - diags = req.State.Get(ctx, &stateModel) + var model Model + diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -819,13 +812,13 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe ctx = core.InitProviderContext(ctx) - projectId := planModel.ProjectId.ValueString() - distributionId := planModel.DistributionId.ValueString() + projectId := model.ProjectId.ValueString() + distributionId := model.DistributionId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "distribution_id", distributionId) - configPlanModel := distributionConfig{} - diags = planModel.Config.As(ctx, &configPlanModel, basetypes.ObjectAsOptions{ + configmodel := distributionConfig{} + diags = model.Config.As(ctx, &configmodel, basetypes.ObjectAsOptions{ UnhandledNullAsEmpty: false, UnhandledUnknownAsEmpty: false, }) @@ -834,18 +827,8 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe return } - configStateModel := distributionConfig{} - diags = stateModel.Config.As(ctx, &configStateModel, basetypes.ObjectAsOptions{ - UnhandledNullAsEmpty: false, - UnhandledUnknownAsEmpty: false, - }) - if diags.HasError() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping state config") - return - } - regions := []cdnSdk.Region{} - for _, r := range *configPlanModel.Regions { + for _, r := range *configmodel.Regions { regionEnum, err := cdnSdk.NewRegionFromValue(r) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Map regions: %v", err)) @@ -856,9 +839,9 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe // blockedCountries var blockedCountries []string - if configPlanModel.BlockedCountries != nil { + if configmodel.BlockedCountries != nil { tempBlockedCountries := []string{} - for _, blockedCountry := range *configPlanModel.BlockedCountries { + for _, blockedCountry := range *configmodel.BlockedCountries { validatedBlockedCountry, err := validateCountryCode(blockedCountry) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Blocked countries: %v", err)) @@ -870,16 +853,16 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } // redirects - redirectsConfig := convertRedirectconfig(configPlanModel.Redirects) + redirectsConfig := convertRedirectconfig(configmodel.Redirects) configPatchBackend := &cdnSdk.ConfigPatchBackend{} - switch configPlanModel.Backend.Type { + switch configmodel.Backend.Type { case "http": geofencingPatch := map[string][]string{} - if configPlanModel.Backend.Geofencing != nil { + if configmodel.Backend.Geofencing != nil { gf := make(map[string][]string) - for url, countries := range *configPlanModel.Backend.Geofencing { + for url, countries := range *configmodel.Backend.Geofencing { countryStrings := make([]string, len(countries)) for i, countryPtr := range countries { if countryPtr == nil { @@ -894,21 +877,21 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } configPatchBackend.HttpBackendPatch = &cdnSdk.HttpBackendPatch{ - OriginRequestHeaders: configPlanModel.Backend.OriginRequestHeaders, - OriginUrl: configPlanModel.Backend.OriginURL, + OriginRequestHeaders: configmodel.Backend.OriginRequestHeaders, + OriginUrl: configmodel.Backend.OriginURL, Type: "http", Geofencing: &geofencingPatch, } case "bucket": configPatchBackend.BucketBackendPatch = &cdnSdk.BucketBackendPatch{ Type: "bucket", - BucketUrl: configPlanModel.Backend.BucketURL, - Region: configPlanModel.Backend.Region, + BucketUrl: configmodel.Backend.BucketURL, + Region: configmodel.Backend.Region, } - if configPlanModel.Backend.Credentials != nil { + if configmodel.Backend.Credentials != nil { configPatchBackend.BucketBackendPatch.Credentials = &cdnSdk.BucketCredentials{ - AccessKeyId: *configPlanModel.Backend.Credentials.AccessKey, - SecretAccessKey: *configPlanModel.Backend.Credentials.SecretKey, + AccessKeyId: *configmodel.Backend.Credentials.AccessKey, + SecretAccessKey: *configmodel.Backend.Credentials.SecretKey, } } } @@ -920,15 +903,21 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Redirects: redirectsConfig, } - // Map WAF Update/Removal - if !utils.IsUndefined(configPlanModel.Waf) { + modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED) + typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE) + wafPatch := cdnSdk.WafConfigPatch{ + Mode: &modeDisabled, + Type: &typeFree, + } + // Map WAF Update + if !utils.IsUndefined(configmodel.Waf) { var wafModel wafConfig - diags := configPlanModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) + diags := configmodel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) if diags.HasError() { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") return } - wafPatch := cdnSdk.WafConfigPatch{ + wafPatch = cdnSdk.WafConfigPatch{ Mode: new(cdnSdk.WafMode(wafModel.Mode.ValueString())), Type: new(cdnSdk.WafType(wafModel.Type.ValueString())), AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions), @@ -949,39 +938,12 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe wafPatch.ParanoiaLevel = &pl } configPatch.Waf = &wafPatch - } else if !utils.IsUndefined(configStateModel.Waf) { - // User explicitly removed the WAF block from their terraform configuration - modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED) - typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE) - var wafModel wafConfig - diags := configStateModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") - return - } - wafPatch := cdnSdk.WafConfigPatch{ - Mode: &modeDisabled, - Type: &typeFree, - AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions), - AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes), - AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods), - EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds), - DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds), - LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds), - EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds), - DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds), - LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds), - EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds), - DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds), - LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds), - } - configPatch.Waf = &wafPatch } - if !utils.IsUndefined(configPlanModel.Optimizer) { + if !utils.IsUndefined(configmodel.Optimizer) { var optimizerModel optimizerConfig - diags = configPlanModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) + diags = configmodel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) if diags.HasError() { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping optimizer config") return @@ -1011,13 +973,13 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe return } - err = mapFields(ctx, &waitResp.Distribution, &planModel) + err = mapFields(ctx, &waitResp.Distribution, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Processing API payload: %v", err)) return } - diags = resp.State.Set(ctx, planModel) + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return From 4a6c09a50c195477d057d28edf9c8241da817bb8 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 11:58:59 +0200 Subject: [PATCH 46/62] chore: add optinal field as computed to waf --- .../services/cdn/distribution/resource.go | 68 +++++++++---------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index a1d6c69b4..c09e80cb8 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -477,11 +477,13 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, "paranoia_level": schema.StringAttribute{ Optional: true, + Computed: true, Description: schemaDescriptions["waf_paranoia_level"], Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafParanoiaLevelEnumValues)...)}, }, "allowed_http_versions": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_versions"], Validators: []validator.List{ @@ -490,6 +492,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, "allowed_request_content_types": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_request_content_types"], Validators: []validator.List{ @@ -505,18 +508,20 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, }, "enabled_rule_ids": schema.ListAttribute{ - Optional: true, - + Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_ids"], }, "disabled_rule_ids": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_ids"], }, "log_only_rule_ids": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_ids"], }, @@ -527,26 +532,31 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, "disabled_rule_group_ids": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_group_ids"], }, "log_only_rule_group_ids": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_group_ids"], }, "enabled_rule_collection_ids": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_collection_ids"], }, "disabled_rule_collection_ids": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_collection_ids"], }, "log_only_rule_collection_ids": schema.ListAttribute{ Optional: true, + Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_collection_ids"], }, @@ -1307,30 +1317,16 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo // Detect if we are running an Import (or Data Source Read) where prior config doesn't exist isImport := utils.IsUndefined(model.Config) - // Parse old WAF state to respect omitted configurations during normal applies - var oldWaf wafConfig - if !isImport { - _ = oldConfig.Waf.As(ctx, &oldWaf, basetypes.ObjectAsOptions{ - UnhandledNullAsEmpty: false, - UnhandledUnknownAsEmpty: false, - }) - } - - // Helper to conditionally map string fields - mapWafString := func(apiVal *string, oldVal types.String) types.String { - // Always map the API value during an Import, OR if the user explicitly defined it in HCL - if isImport || !oldVal.IsNull() { - if apiVal != nil { - return types.StringValue(*apiVal) - } + // Helper to unconditionally map string fields + mapWafString := func(apiVal *string) types.String { + if apiVal != nil { + return types.StringValue(*apiVal) } return types.StringNull() } - - // Helper to conditionally map list fields - mapWafList := func(apiList []string, oldList types.List) types.List { - // Always map the API value during an Import, OR if the user explicitly defined it in HCL - if isImport || !oldList.IsNull() { + // Helper to unconditionally map list fields + mapWafList := func(apiList []string) types.List { + if apiList != nil { return conversion.SortedStringsToListValue(apiList) } return types.ListNull(types.StringType) @@ -1341,19 +1337,19 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo plVal := string(*distribution.Config.Waf.ParanoiaLevel) pl = &plVal } - wafObjAttrs["paranoia_level"] = mapWafString(pl, oldWaf.ParanoiaLevel) - wafObjAttrs["allowed_http_versions"] = mapWafList(distribution.Config.Waf.AllowedHttpVersions, oldWaf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafList(distribution.Config.Waf.AllowedRequestContentTypes, oldWaf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafList(distribution.Config.Waf.AllowedHttpMethods, oldWaf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleIds, oldWaf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleIds, oldWaf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleIds, oldWaf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleGroupIds, oldWaf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleGroupIds, oldWaf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleGroupIds, oldWaf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleCollectionIds, oldWaf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleCollectionIds, oldWaf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleCollectionIds, oldWaf.LogOnlyRuleCollectionIds) + wafObjAttrs["paranoia_level"] = mapWafString(pl) + wafObjAttrs["allowed_http_versions"] = mapWafList(distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafList(distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafList(distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleCollectionIds) // Determine if WAF should be entirely excluded to prevent drift. // The API can return an empty string for fully unconfigured backends. From 84bd06ca88c112df311abaaadbc875e1cafe7fa2 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 12:02:09 +0200 Subject: [PATCH 47/62] chore: add http method as computed --- stackit/internal/services/cdn/distribution/resource.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index c09e80cb8..9bc8f43f4 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -502,6 +502,7 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques "allowed_http_methods": schema.ListAttribute{ Optional: true, ElementType: types.StringType, + Computed: true, Description: schemaDescriptions["waf_allowed_http_methods"], Validators: []validator.List{ listvalidator.SizeAtLeast(1), From f74f399589d9889d205959c8dcdc20e6b99af7a6 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 13:32:34 +0200 Subject: [PATCH 48/62] chore: swich the list in wat to set --- .../services/cdn/distribution/resource.go | 201 +++++++++--------- 1 file changed, 103 insertions(+), 98 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 9bc8f43f4..1c095ffb2 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -15,6 +15,7 @@ import ( "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/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -90,18 +91,18 @@ var schemaDescriptions = map[string]string{ "waf_mode": "The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'.", "waf_type": "The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'.", "waf_paranoia_level": "Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'.", - "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`.", - "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`.", - "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`.", - "waf_enabled_rule_ids": "List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_ids": "List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_ids": "List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_group_ids": "List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_group_ids": "List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_group_ids": "List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_enabled_rule_collection_ids": "List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_disabled_rule_collection_ids": "List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", - "waf_log_only_rule_collection_ids": "List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`.", + "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`.", + "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`.", + "waf_enabled_rule_ids": "Set of WAF rule IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_ids": "Set of WAF rule IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_ids": "Set of WAF rule IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_group_ids": "Set of WAF Rule Group IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_group_ids": "Set of WAF Rule Group IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_group_ids": "Set of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_collection_ids": "Set of WAF Collection IDs explicitly enabled. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_collection_ids": "Set of WAF Collection IDs explicitly disabled. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_collection_ids": "Set of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", } type Model struct { @@ -161,18 +162,18 @@ type wafConfig struct { Mode types.String `tfsdk:"mode"` Type types.String `tfsdk:"type"` ParanoiaLevel types.String `tfsdk:"paranoia_level"` - AllowedHttpVersions types.List `tfsdk:"allowed_http_versions"` - AllowedRequestContentTypes types.List `tfsdk:"allowed_request_content_types"` - AllowedHttpMethods types.List `tfsdk:"allowed_http_methods"` - EnabledRuleIds types.List `tfsdk:"enabled_rule_ids"` - DisabledRuleIds types.List `tfsdk:"disabled_rule_ids"` - LogOnlyRuleIds types.List `tfsdk:"log_only_rule_ids"` - EnabledRuleGroupIds types.List `tfsdk:"enabled_rule_group_ids"` - DisabledRuleGroupIds types.List `tfsdk:"disabled_rule_group_ids"` - LogOnlyRuleGroupIds types.List `tfsdk:"log_only_rule_group_ids"` - EnabledRuleCollectionIds types.List `tfsdk:"enabled_rule_collection_ids"` - DisabledRuleCollectionIds types.List `tfsdk:"disabled_rule_collection_ids"` - LogOnlyRuleCollectionIds types.List `tfsdk:"log_only_rule_collection_ids"` + AllowedHttpVersions types.Set `tfsdk:"allowed_http_versions"` + AllowedRequestContentTypes types.Set `tfsdk:"allowed_request_content_types"` + AllowedHttpMethods types.Set `tfsdk:"allowed_http_methods"` + EnabledRuleIds types.Set `tfsdk:"enabled_rule_ids"` + DisabledRuleIds types.Set `tfsdk:"disabled_rule_ids"` + LogOnlyRuleIds types.Set `tfsdk:"log_only_rule_ids"` + EnabledRuleGroupIds types.Set `tfsdk:"enabled_rule_group_ids"` + DisabledRuleGroupIds types.Set `tfsdk:"disabled_rule_group_ids"` + LogOnlyRuleGroupIds types.Set `tfsdk:"log_only_rule_group_ids"` + EnabledRuleCollectionIds types.Set `tfsdk:"enabled_rule_collection_ids"` + DisabledRuleCollectionIds types.Set `tfsdk:"disabled_rule_collection_ids"` + LogOnlyRuleCollectionIds types.Set `tfsdk:"log_only_rule_collection_ids"` } type backendCredentials struct { @@ -233,18 +234,18 @@ var wafTypes = map[string]attr.Type{ "mode": types.StringType, "type": types.StringType, "paranoia_level": types.StringType, - "allowed_http_versions": types.ListType{ElemType: types.StringType}, - "allowed_request_content_types": types.ListType{ElemType: types.StringType}, - "allowed_http_methods": types.ListType{ElemType: types.StringType}, - "enabled_rule_ids": types.ListType{ElemType: types.StringType}, - "disabled_rule_ids": types.ListType{ElemType: types.StringType}, - "log_only_rule_ids": types.ListType{ElemType: types.StringType}, - "enabled_rule_group_ids": types.ListType{ElemType: types.StringType}, - "disabled_rule_group_ids": types.ListType{ElemType: types.StringType}, - "log_only_rule_group_ids": types.ListType{ElemType: types.StringType}, - "enabled_rule_collection_ids": types.ListType{ElemType: types.StringType}, - "disabled_rule_collection_ids": types.ListType{ElemType: types.StringType}, - "log_only_rule_collection_ids": types.ListType{ElemType: types.StringType}, + "allowed_http_versions": types.SetType{ElemType: types.StringType}, + "allowed_request_content_types": types.SetType{ElemType: types.StringType}, + "allowed_http_methods": types.SetType{ElemType: types.StringType}, + "enabled_rule_ids": types.SetType{ElemType: types.StringType}, + "disabled_rule_ids": types.SetType{ElemType: types.StringType}, + "log_only_rule_ids": types.SetType{ElemType: types.StringType}, + "enabled_rule_group_ids": types.SetType{ElemType: types.StringType}, + "disabled_rule_group_ids": types.SetType{ElemType: types.StringType}, + "log_only_rule_group_ids": types.SetType{ElemType: types.StringType}, + "enabled_rule_collection_ids": types.SetType{ElemType: types.StringType}, + "disabled_rule_collection_ids": types.SetType{ElemType: types.StringType}, + "log_only_rule_collection_ids": types.SetType{ElemType: types.StringType}, } var backendTypes = map[string]attr.Type{ @@ -481,81 +482,81 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Description: schemaDescriptions["waf_paranoia_level"], Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafParanoiaLevelEnumValues)...)}, }, - "allowed_http_versions": schema.ListAttribute{ + "allowed_http_versions": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_versions"], - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), }, }, - "allowed_request_content_types": schema.ListAttribute{ + "allowed_request_content_types": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_request_content_types"], - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), }, }, - "allowed_http_methods": schema.ListAttribute{ + "allowed_http_methods": schema.SetAttribute{ Optional: true, ElementType: types.StringType, Computed: true, Description: schemaDescriptions["waf_allowed_http_methods"], - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), }, }, - "enabled_rule_ids": schema.ListAttribute{ + "enabled_rule_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_ids"], }, - "disabled_rule_ids": schema.ListAttribute{ + "disabled_rule_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_ids"], }, - "log_only_rule_ids": schema.ListAttribute{ + "log_only_rule_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_ids"], }, - "enabled_rule_group_ids": schema.ListAttribute{ + "enabled_rule_group_ids": schema.SetAttribute{ Optional: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_group_ids"], }, - "disabled_rule_group_ids": schema.ListAttribute{ + "disabled_rule_group_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_group_ids"], }, - "log_only_rule_group_ids": schema.ListAttribute{ + "log_only_rule_group_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_group_ids"], }, - "enabled_rule_collection_ids": schema.ListAttribute{ + "enabled_rule_collection_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_collection_ids"], }, - "disabled_rule_collection_ids": schema.ListAttribute{ + "disabled_rule_collection_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_collection_ids"], }, - "log_only_rule_collection_ids": schema.ListAttribute{ + "log_only_rule_collection_ids": schema.SetAttribute{ Optional: true, Computed: true, ElementType: types.StringType, @@ -931,18 +932,18 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe wafPatch = cdnSdk.WafConfigPatch{ Mode: new(cdnSdk.WafMode(wafModel.Mode.ValueString())), Type: new(cdnSdk.WafType(wafModel.Type.ValueString())), - AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions), - AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes), - AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods), - EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds), - DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds), - LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds), - EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds), - DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds), - LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds), - EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds), - DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds), - LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds), + AllowedHttpVersions: getWafSet(ctx, wafModel.AllowedHttpVersions), + AllowedRequestContentTypes: getWafSet(ctx, wafModel.AllowedRequestContentTypes), + AllowedHttpMethods: getWafSet(ctx, wafModel.AllowedHttpMethods), + EnabledRuleIds: getWafSet(ctx, wafModel.EnabledRuleIds), + DisabledRuleIds: getWafSet(ctx, wafModel.DisabledRuleIds), + LogOnlyRuleIds: getWafSet(ctx, wafModel.LogOnlyRuleIds), + EnabledRuleGroupIds: getWafSet(ctx, wafModel.EnabledRuleGroupIds), + DisabledRuleGroupIds: getWafSet(ctx, wafModel.DisabledRuleGroupIds), + LogOnlyRuleGroupIds: getWafSet(ctx, wafModel.LogOnlyRuleGroupIds), + EnabledRuleCollectionIds: getWafSet(ctx, wafModel.EnabledRuleCollectionIds), + DisabledRuleCollectionIds: getWafSet(ctx, wafModel.DisabledRuleCollectionIds), + LogOnlyRuleCollectionIds: getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds), } if !utils.IsUndefined(wafModel.ParanoiaLevel) { pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString()) @@ -1325,12 +1326,16 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo } return types.StringNull() } - // Helper to unconditionally map list fields - mapWafList := func(apiList []string) types.List { + + // Helper to unconditionally map set fields + mapWafSet := func(apiList []string) types.Set { if apiList != nil { - return conversion.SortedStringsToListValue(apiList) + setVal, diags := types.SetValueFrom(ctx, types.StringType, apiList) + if !diags.HasError() { + return setVal + } } - return types.ListNull(types.StringType) + return types.SetNull(types.StringType) } var pl *string @@ -1339,18 +1344,18 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo pl = &plVal } wafObjAttrs["paranoia_level"] = mapWafString(pl) - wafObjAttrs["allowed_http_versions"] = mapWafList(distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafList(distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafList(distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafList(distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafList(distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = mapWafSet(distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafSet(distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafSet(distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleCollectionIds) // Determine if WAF should be entirely excluded to prevent drift. // The API can return an empty string for fully unconfigured backends. @@ -1669,18 +1674,18 @@ func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { cdnConfig.Waf = cdnSdk.WafConfig{ Mode: cdnSdk.WafMode(wafModel.Mode.ValueString()), Type: cdnSdk.WafType(wafModel.Type.ValueString()), - AllowedHttpVersions: getSortedWafList(ctx, wafModel.AllowedHttpVersions), - AllowedRequestContentTypes: getSortedWafList(ctx, wafModel.AllowedRequestContentTypes), - AllowedHttpMethods: getSortedWafList(ctx, wafModel.AllowedHttpMethods), - EnabledRuleIds: getSortedWafList(ctx, wafModel.EnabledRuleIds), - DisabledRuleIds: getSortedWafList(ctx, wafModel.DisabledRuleIds), - LogOnlyRuleIds: getSortedWafList(ctx, wafModel.LogOnlyRuleIds), - EnabledRuleGroupIds: getSortedWafList(ctx, wafModel.EnabledRuleGroupIds), - DisabledRuleGroupIds: getSortedWafList(ctx, wafModel.DisabledRuleGroupIds), - LogOnlyRuleGroupIds: getSortedWafList(ctx, wafModel.LogOnlyRuleGroupIds), - EnabledRuleCollectionIds: getSortedWafList(ctx, wafModel.EnabledRuleCollectionIds), - DisabledRuleCollectionIds: getSortedWafList(ctx, wafModel.DisabledRuleCollectionIds), - LogOnlyRuleCollectionIds: getSortedWafList(ctx, wafModel.LogOnlyRuleCollectionIds), + AllowedHttpVersions: getWafSet(ctx, wafModel.AllowedHttpVersions), + AllowedRequestContentTypes: getWafSet(ctx, wafModel.AllowedRequestContentTypes), + AllowedHttpMethods: getWafSet(ctx, wafModel.AllowedHttpMethods), + EnabledRuleIds: getWafSet(ctx, wafModel.EnabledRuleIds), + DisabledRuleIds: getWafSet(ctx, wafModel.DisabledRuleIds), + LogOnlyRuleIds: getWafSet(ctx, wafModel.LogOnlyRuleIds), + EnabledRuleGroupIds: getWafSet(ctx, wafModel.EnabledRuleGroupIds), + DisabledRuleGroupIds: getWafSet(ctx, wafModel.DisabledRuleGroupIds), + LogOnlyRuleGroupIds: getWafSet(ctx, wafModel.LogOnlyRuleGroupIds), + EnabledRuleCollectionIds: getWafSet(ctx, wafModel.EnabledRuleCollectionIds), + DisabledRuleCollectionIds: getWafSet(ctx, wafModel.DisabledRuleCollectionIds), + LogOnlyRuleCollectionIds: getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds), } if !utils.IsUndefined(wafModel.ParanoiaLevel) { @@ -1746,13 +1751,13 @@ func validateCountryCode(country string) (string, error) { return upperCountry, nil } -// getSortedWafList extracts strings from HCL list, sorts them and returns the slice -func getSortedWafList(ctx context.Context, tfList basetypes.ListValue) []string { - if utils.IsUndefined(tfList) { +// getWafSet extracts strings from HCL set, sorts them and returns the slice +func getWafSet(ctx context.Context, tfSet basetypes.SetValue) []string { + if utils.IsUndefined(tfSet) { return nil } var elements []string - diags := tfList.ElementsAs(ctx, &elements, true) + diags := tfSet.ElementsAs(ctx, &elements, true) if diags.HasError() { return []string{} } From fa27007cc1d937a072962ae0ab8e0daebca71c1b Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 17:04:55 +0200 Subject: [PATCH 49/62] chore: improve update --- .../services/cdn/distribution/resource.go | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 1c095ffb2..17e04dd82 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -917,10 +917,11 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED) typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE) - wafPatch := cdnSdk.WafConfigPatch{ + configPatch.Waf = &cdnSdk.WafConfigPatch{ Mode: &modeDisabled, Type: &typeFree, } + // Map WAF Update if !utils.IsUndefined(configmodel.Waf) { var wafModel wafConfig @@ -929,27 +930,25 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") return } - wafPatch = cdnSdk.WafConfigPatch{ - Mode: new(cdnSdk.WafMode(wafModel.Mode.ValueString())), - Type: new(cdnSdk.WafType(wafModel.Type.ValueString())), - AllowedHttpVersions: getWafSet(ctx, wafModel.AllowedHttpVersions), - AllowedRequestContentTypes: getWafSet(ctx, wafModel.AllowedRequestContentTypes), - AllowedHttpMethods: getWafSet(ctx, wafModel.AllowedHttpMethods), - EnabledRuleIds: getWafSet(ctx, wafModel.EnabledRuleIds), - DisabledRuleIds: getWafSet(ctx, wafModel.DisabledRuleIds), - LogOnlyRuleIds: getWafSet(ctx, wafModel.LogOnlyRuleIds), - EnabledRuleGroupIds: getWafSet(ctx, wafModel.EnabledRuleGroupIds), - DisabledRuleGroupIds: getWafSet(ctx, wafModel.DisabledRuleGroupIds), - LogOnlyRuleGroupIds: getWafSet(ctx, wafModel.LogOnlyRuleGroupIds), - EnabledRuleCollectionIds: getWafSet(ctx, wafModel.EnabledRuleCollectionIds), - DisabledRuleCollectionIds: getWafSet(ctx, wafModel.DisabledRuleCollectionIds), - LogOnlyRuleCollectionIds: getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds), - } + configPatch.Waf.Mode = new(cdnSdk.WafMode(wafModel.Mode.ValueString())) + configPatch.Waf.Type = new(cdnSdk.WafType(wafModel.Type.ValueString())) + configPatch.Waf.AllowedHttpVersions = getWafSet(ctx, wafModel.AllowedHttpVersions) + configPatch.Waf.AllowedRequestContentTypes = getWafSet(ctx, wafModel.AllowedRequestContentTypes) + configPatch.Waf.AllowedHttpMethods = getWafSet(ctx, wafModel.AllowedHttpMethods) + configPatch.Waf.EnabledRuleIds = getWafSet(ctx, wafModel.EnabledRuleIds) + configPatch.Waf.DisabledRuleIds = getWafSet(ctx, wafModel.DisabledRuleIds) + configPatch.Waf.LogOnlyRuleIds = getWafSet(ctx, wafModel.LogOnlyRuleIds) + configPatch.Waf.EnabledRuleGroupIds = getWafSet(ctx, wafModel.EnabledRuleGroupIds) + configPatch.Waf.DisabledRuleGroupIds = getWafSet(ctx, wafModel.DisabledRuleGroupIds) + configPatch.Waf.LogOnlyRuleGroupIds = getWafSet(ctx, wafModel.LogOnlyRuleGroupIds) + configPatch.Waf.EnabledRuleCollectionIds = getWafSet(ctx, wafModel.EnabledRuleCollectionIds) + configPatch.Waf.DisabledRuleCollectionIds = getWafSet(ctx, wafModel.DisabledRuleCollectionIds) + configPatch.Waf.LogOnlyRuleCollectionIds = getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds) + if !utils.IsUndefined(wafModel.ParanoiaLevel) { pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString()) - wafPatch.ParanoiaLevel = &pl + configPatch.Waf.ParanoiaLevel = &pl } - configPatch.Waf = &wafPatch } if !utils.IsUndefined(configmodel.Optimizer) { From fddccee81df879407e3963eca9d9c2c7757759f3 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 17:14:53 +0200 Subject: [PATCH 50/62] chore: swich to set in datasource --- .../services/cdn/distribution/datasource.go | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index a23e03777..c962ad620 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -274,62 +274,62 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe Computed: true, Description: schemaDescriptions["waf_paranoia_level"], }, - "allowed_http_versions": schema.ListAttribute{ + "allowed_http_versions": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_versions"], }, - "allowed_request_content_types": schema.ListAttribute{ + "allowed_request_content_types": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_request_content_types"], }, - "allowed_http_methods": schema.ListAttribute{ + "allowed_http_methods": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_allowed_http_methods"], }, - "enabled_rule_ids": schema.ListAttribute{ + "enabled_rule_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_ids"], }, - "disabled_rule_ids": schema.ListAttribute{ + "disabled_rule_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_ids"], }, - "log_only_rule_ids": schema.ListAttribute{ + "log_only_rule_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_ids"], }, - "enabled_rule_group_ids": schema.ListAttribute{ + "enabled_rule_group_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_group_ids"], }, - "disabled_rule_group_ids": schema.ListAttribute{ + "disabled_rule_group_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_group_ids"], }, - "log_only_rule_group_ids": schema.ListAttribute{ + "log_only_rule_group_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_group_ids"], }, - "enabled_rule_collection_ids": schema.ListAttribute{ + "enabled_rule_collection_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_enabled_rule_collection_ids"], }, - "disabled_rule_collection_ids": schema.ListAttribute{ + "disabled_rule_collection_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_disabled_rule_collection_ids"], }, - "log_only_rule_collection_ids": schema.ListAttribute{ + "log_only_rule_collection_ids": schema.SetAttribute{ Computed: true, ElementType: types.StringType, Description: schemaDescriptions["waf_log_only_rule_collection_ids"], @@ -593,6 +593,17 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, return core.DiagsToError(diags) } + // Helper to unconditionally map set fields for WAF + mapWafSet := func(apiList []string) types.Set { + if apiList != nil { + setVal, diags := types.SetValueFrom(ctx, types.StringType, apiList) + if !diags.HasError() { + return setVal + } + } + return types.SetNull(types.StringType) + } + // Map Waf wafVal := types.ObjectNull(wafTypes) if distribution.Config.Waf.Mode != "" { @@ -607,18 +618,18 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, wafObjAttrs["paranoia_level"] = types.StringNull() } - wafObjAttrs["allowed_http_versions"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = conversion.SortedStringsToListValue(distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = conversion.SortedStringsToListValue(distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = mapWafSet(distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafSet(distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafSet(distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleCollectionIds) var diagWaf diag.Diagnostics wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) From 1cd663390d689061f0abfc31a377d2eb3b2c4fee Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 17:23:05 +0200 Subject: [PATCH 51/62] chore: adjsut the resouce and datasource cdn test to the new set type --- .../cdn/distribution/datasource_test.go | 26 ++--- .../cdn/distribution/resource_test.go | 104 +++++++++--------- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/datasource_test.go b/stackit/internal/services/cdn/distribution/datasource_test.go index 1252bf997..5124f4e29 100644 --- a/stackit/internal/services/cdn/distribution/datasource_test.go +++ b/stackit/internal/services/cdn/distribution/datasource_test.go @@ -96,7 +96,7 @@ func TestMapDataSourceFields(t *testing.T) { domains := types.ListValueMust(types.ObjectType{AttrTypes: domainTypes}, []attr.Value{managedDomain}) // WAF Fixtures - populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ types.StringValue("rule1"), types.StringValue("rule2"), }) @@ -104,18 +104,18 @@ func TestMapDataSourceFields(t *testing.T) { "mode": types.StringValue("ENABLED"), "type": types.StringValue("PREMIUM"), "paranoia_level": types.StringValue("L2"), - "allowed_http_versions": populatedWafList, - "allowed_request_content_types": populatedWafList, - "allowed_http_methods": populatedWafList, - "enabled_rule_ids": populatedWafList, - "disabled_rule_ids": populatedWafList, - "log_only_rule_ids": populatedWafList, - "enabled_rule_group_ids": populatedWafList, - "disabled_rule_group_ids": populatedWafList, - "log_only_rule_group_ids": populatedWafList, - "enabled_rule_collection_ids": populatedWafList, - "disabled_rule_collection_ids": populatedWafList, - "log_only_rule_collection_ids": populatedWafList, + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, }) expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index 9449f4d74..f95eda7d0 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -43,7 +43,7 @@ func TestToCreatePayload(t *testing.T) { optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(true), }) - emptyWafList := types.ListValueMust(types.StringType, []attr.Value{}) + emptyWafSet := types.SetValueMust(types.StringType, []attr.Value{}) expectedDefaultWafConfig := cdnSdk.WafConfig{ Mode: cdnSdk.WafMode("DISABLED"), Type: cdnSdk.WafType("FREE"), @@ -64,18 +64,18 @@ func TestToCreatePayload(t *testing.T) { "mode": types.StringValue("DISABLED"), "type": types.StringValue("FREE"), "paranoia_level": types.StringNull(), - "allowed_http_versions": emptyWafList, - "allowed_request_content_types": emptyWafList, - "allowed_http_methods": emptyWafList, - "enabled_rule_ids": emptyWafList, - "disabled_rule_ids": emptyWafList, - "log_only_rule_ids": emptyWafList, - "enabled_rule_group_ids": emptyWafList, - "disabled_rule_group_ids": emptyWafList, - "log_only_rule_group_ids": emptyWafList, - "enabled_rule_collection_ids": emptyWafList, - "disabled_rule_collection_ids": emptyWafList, - "log_only_rule_collection_ids": emptyWafList, + "allowed_http_versions": emptyWafSet, + "allowed_request_content_types": emptyWafSet, + "allowed_http_methods": emptyWafSet, + "enabled_rule_ids": emptyWafSet, + "disabled_rule_ids": emptyWafSet, + "log_only_rule_ids": emptyWafSet, + "enabled_rule_group_ids": emptyWafSet, + "disabled_rule_group_ids": emptyWafSet, + "log_only_rule_group_ids": emptyWafSet, + "enabled_rule_collection_ids": emptyWafSet, + "disabled_rule_collection_ids": emptyWafSet, + "log_only_rule_collection_ids": emptyWafSet, }) redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) if !ok { @@ -114,7 +114,7 @@ func TestToCreatePayload(t *testing.T) { redirectsConfigVal := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{ "rules": rulesList, }) - populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ types.StringValue("rule1"), types.StringValue("rule2"), }) @@ -122,18 +122,18 @@ func TestToCreatePayload(t *testing.T) { "mode": types.StringValue("ENABLED"), "type": types.StringValue("PREMIUM"), "paranoia_level": types.StringValue("L2"), - "allowed_http_versions": populatedWafList, - "allowed_request_content_types": populatedWafList, - "allowed_http_methods": populatedWafList, - "enabled_rule_ids": populatedWafList, - "disabled_rule_ids": populatedWafList, - "log_only_rule_ids": populatedWafList, - "enabled_rule_group_ids": populatedWafList, - "disabled_rule_group_ids": populatedWafList, - "log_only_rule_group_ids": populatedWafList, - "enabled_rule_collection_ids": populatedWafList, - "disabled_rule_collection_ids": populatedWafList, - "log_only_rule_collection_ids": populatedWafList, + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, }) expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") @@ -416,7 +416,7 @@ func TestConvertConfig(t *testing.T) { "matchers": matchersList, }) rulesList := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleVal}) - populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ types.StringValue("rule1"), types.StringValue("rule2"), }) @@ -428,18 +428,18 @@ func TestConvertConfig(t *testing.T) { "mode": types.StringValue("ENABLED"), "type": types.StringValue("PREMIUM"), "paranoia_level": types.StringValue("L2"), - "allowed_http_versions": populatedWafList, - "allowed_request_content_types": populatedWafList, - "allowed_http_methods": populatedWafList, - "enabled_rule_ids": populatedWafList, - "disabled_rule_ids": populatedWafList, - "log_only_rule_ids": populatedWafList, - "enabled_rule_group_ids": populatedWafList, - "disabled_rule_group_ids": populatedWafList, - "log_only_rule_group_ids": populatedWafList, - "enabled_rule_collection_ids": populatedWafList, - "disabled_rule_collection_ids": populatedWafList, - "log_only_rule_collection_ids": populatedWafList, + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, }) expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") @@ -721,7 +721,7 @@ func TestMapFields(t *testing.T) { t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") } redirectsAttrTypes := redirectsObjType.AttrTypes - populatedWafList := types.ListValueMust(types.StringType, []attr.Value{ + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ types.StringValue("rule1"), types.StringValue("rule2"), }) @@ -729,18 +729,18 @@ func TestMapFields(t *testing.T) { "mode": types.StringValue("ENABLED"), "type": types.StringValue("PREMIUM"), "paranoia_level": types.StringValue("L2"), - "allowed_http_versions": populatedWafList, - "allowed_request_content_types": populatedWafList, - "allowed_http_methods": populatedWafList, - "enabled_rule_ids": populatedWafList, - "disabled_rule_ids": populatedWafList, - "log_only_rule_ids": populatedWafList, - "enabled_rule_group_ids": populatedWafList, - "disabled_rule_group_ids": populatedWafList, - "log_only_rule_group_ids": populatedWafList, - "enabled_rule_collection_ids": populatedWafList, - "disabled_rule_collection_ids": populatedWafList, - "log_only_rule_collection_ids": populatedWafList, + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, }) expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") From 7f734dc2c284a2959c2e9f9cb3f7331721aa7087 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Tue, 5 May 2026 17:28:32 +0200 Subject: [PATCH 52/62] chore: update doc --- docs/data-sources/cdn_distribution.md | 24 ++++++++++++------------ docs/resources/cdn_distribution.md | 26 +++++++++++++------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 4ae8a4a76..d6ab74648 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -111,18 +111,18 @@ Read-Only: Read-Only: -- `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. -- `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. -- `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `allowed_http_methods` (Set of String) Restricts which HTTP methods the distribution accepts. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. +- `allowed_http_versions` (Set of String) Restricts which HTTP protocol versions are accepted. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. +- `allowed_request_content_types` (Set of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. +- `disabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly disabled. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly enabled. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (Set of String) Set of WAF rule IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. - `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index b865040f6..ea17a0dab 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -83,7 +83,7 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { # "@builtin/crs/request/942151" is explicitly DISABLED, overriding the collection setting. # # To view all available collections, groups, and rules, consult the API documentation: - # https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections. + # https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections waf = { mode = "ENABLED" type = "PREMIUM" @@ -230,18 +230,18 @@ Required: Optional: -- `allowed_http_methods` (List of String) Restricts which HTTP methods the distribution accepts. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. -- `allowed_http_versions` (List of String) Restricts which HTTP protocol versions are accepted. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. -- `allowed_request_content_types` (List of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the list must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. -- `disabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly disabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `disabled_rule_ids` (List of String) List of WAF rule IDs explicitly disabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly enabled. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `enabled_rule_ids` (List of String) List of WAF rule IDs explicitly enabled. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_collection_ids` (List of String) List of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_group_ids` (List of String) List of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections -- `log_only_rule_ids` (List of String) List of WAF rule IDs explicitly marked as Log Only. Can be set to an empty list to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `allowed_http_methods` (Set of String) Restricts which HTTP methods the distribution accepts. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. +- `allowed_http_versions` (Set of String) Restricts which HTTP protocol versions are accepted. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. +- `allowed_request_content_types` (Set of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. +- `disabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly disabled. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly enabled. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (Set of String) Set of WAF rule IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections - `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. From 7db83f2a2ce440f64bd68390ab1e8bc8efdf4e94 Mon Sep 17 00:00:00 2001 From: Politano <47066134+matheuspolitano@users.noreply.github.com> Date: Wed, 6 May 2026 12:01:28 +0200 Subject: [PATCH 53/62] Update stackit/internal/services/cdn/distribution/resource.go Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- stackit/internal/services/cdn/distribution/resource.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 17e04dd82..af80bce3d 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -915,11 +915,9 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Redirects: redirectsConfig, } - modeDisabled := cdnSdk.WafMode(cdnSdk.WAFMODE_DISABLED) - typeFree := cdnSdk.WafType(cdnSdk.WAFTYPE_FREE) configPatch.Waf = &cdnSdk.WafConfigPatch{ - Mode: &modeDisabled, - Type: &typeFree, + Mode: new(cdnSdk.WAFMODE_DISABLED), + Type: new(cdnSdk.WAFTYPE_FREE), } // Map WAF Update From 49ddaeb40361a778d2720473cd618b4abcbd138d Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 12:16:39 +0200 Subject: [PATCH 54/62] chore: remove conversion --- stackit/internal/conversion/conversion.go | 42 +----- .../internal/conversion/conversion_test.go | 130 ------------------ 2 files changed, 1 insertion(+), 171 deletions(-) diff --git a/stackit/internal/conversion/conversion.go b/stackit/internal/conversion/conversion.go index 936e5a591..11bdd5994 100644 --- a/stackit/internal/conversion/conversion.go +++ b/stackit/internal/conversion/conversion.go @@ -10,7 +10,6 @@ import ( "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/terraform-provider-stackit/stackit/internal/core" ) @@ -109,16 +108,6 @@ func Int64ValueToPointer(s basetypes.Int64Value) *int64 { return &value } -// Float32ValueToPointer converts basetypes.Float32Value to a pointer to float32. -// It returns nil if the value is null or unknown. -func Float32ValueToPointer(s basetypes.Float32Value) *float32 { - if s.IsNull() || s.IsUnknown() { - return nil - } - value := s.ValueFloat32() - return &value -} - // Float64ValueToPointer converts basetypes.Float64Value to a pointer to float64. // It returns nil if the value is null or unknown. func Float64ValueToPointer(s basetypes.Float64Value) *float64 { @@ -142,21 +131,10 @@ func BoolValueToPointer(s basetypes.BoolValue) *bool { // StringListToPointer converts basetypes.ListValue to a pointer to a list of strings. // It returns nil if the value is null or unknown. func StringListToPointer(list basetypes.ListValue) (*[]string, error) { - result, err := StringListToSlice(list) - if result == nil { - return nil, err - } - return &result, err -} - -// StringListToSlice converts basetypes.ListValue to a list of strings. -// It returns nil if the value is null or unknown. -func StringListToSlice(list basetypes.ListValue) ([]string, error) { if list.IsNull() || list.IsUnknown() { return nil, nil } - // Instantiate an empty slice to ensure the slice is not nil listStr := []string{} for i, el := range list.Elements() { elStr, ok := el.(types.String) @@ -166,7 +144,7 @@ func StringListToSlice(list basetypes.ListValue) ([]string, error) { listStr = append(listStr, elStr.ValueString()) } - return listStr, nil + return &listStr, nil } // StringSetToPointer converts basetypes.SetValue to a pointer to a list of strings. @@ -263,21 +241,3 @@ func ParseEphemeralProviderData(ctx context.Context, providerData any, diags *di } return stackitProviderData, true } - -// SortedStringsToListValue guarantees the returned HCL List is sorted -func SortedStringsToListValue(items []string) basetypes.ListValue { - if items == nil { - return types.ListNull(types.StringType) - } - if len(items) == 0 { - return types.ListValueMust(types.StringType, []attr.Value{}) - } - sorted := make([]string, len(items)) - copy(sorted, items) - sort.Strings(sorted) - elements := make([]attr.Value, len(sorted)) - for i, val := range sorted { - elements[i] = types.StringValue(val) - } - return types.ListValueMust(types.StringType, elements) -} diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index b48425ad3..c1dfe8763 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/google/go-cmp/cmp" @@ -450,132 +449,3 @@ func TestStringSetToSlice(t *testing.T) { }) } } - -func TestStringListToSlice(t *testing.T) { - t.Parallel() - tests := []struct { - name string - in basetypes.ListValue - want []string - wantErr bool - }{ - { - name: "unknown", - in: basetypes.NewListUnknown(types.StringType), - want: nil, - }, - { - name: "null", - in: basetypes.NewListNull(types.StringType), - want: nil, - }, - { - name: "empty list", - in: basetypes.NewListValueMust(types.StringType, []attr.Value{}), - want: []string{}, - }, - { - name: "invalid type", - in: basetypes.NewListValueMust(types.Int64Type, []attr.Value{types.Int64Value(123)}), - wantErr: true, - }, - { - name: "some values", - in: basetypes.NewListValueMust( - types.StringType, - []attr.Value{ - types.StringValue("abc"), - types.StringValue("xyz"), - }, - ), - want: []string{ - "abc", - "xyz", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, err := StringListToSlice(tt.in) - if tt.wantErr && err == nil { - t.Fatal("expected error") - } - if !tt.wantErr && err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if d := cmp.Diff(got, tt.want); d != "" { - t.Fatalf("no match, diff: %s", d) - } - }) - } -} - -func TestSortedStringsToListValue(t *testing.T) { - tests := []struct { - name string - items []string - expected basetypes.ListValue - }{ - { - name: "empty slice", - items: []string{}, - expected: types.ListValueMust(types.StringType, []attr.Value{}), - }, - { - name: "nil slice", - items: nil, - expected: types.ListNull(types.StringType), - }, - { - name: "unsorted slice", - items: []string{"c", "a", "b"}, - expected: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("a"), - types.StringValue("b"), - types.StringValue("c"), - }), - }, - { - name: "already sorted slice", - items: []string{"a", "b", "c"}, - expected: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("a"), - types.StringValue("b"), - types.StringValue("c"), - }), - }, - { - name: "with duplicates", - items: []string{"b", "a", "b"}, - expected: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("a"), - types.StringValue("b"), - types.StringValue("b"), - }), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Keep a copy of original to ensure it's not mutated by the function - var original []string - if tt.items != nil { - original = make([]string, len(tt.items)) - copy(original, tt.items) - } - - actual := SortedStringsToListValue(tt.items) - - if !actual.Equal(tt.expected) { - t.Errorf("expected %v, got %v", tt.expected, actual) - } - - // Verify original slice was not mutated - if !reflect.DeepEqual(original, tt.items) { - t.Errorf("original slice was mutated: expected %v, got %v", original, tt.items) - } - }) - } -} From 7860ee62896930769610db1276d6e03c6931d267 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 12:19:06 +0200 Subject: [PATCH 55/62] chore: remove the sort --- stackit/internal/services/cdn/distribution/resource.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 17e04dd82..c3a530b6f 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -6,7 +6,6 @@ import ( "fmt" "maps" "net/http" - "sort" "strings" "time" @@ -1750,7 +1749,7 @@ func validateCountryCode(country string) (string, error) { return upperCountry, nil } -// getWafSet extracts strings from HCL set, sorts them and returns the slice +// getWafSet extracts strings from HCL set func getWafSet(ctx context.Context, tfSet basetypes.SetValue) []string { if utils.IsUndefined(tfSet) { return nil @@ -1760,6 +1759,5 @@ func getWafSet(ctx context.Context, tfSet basetypes.SetValue) []string { if diags.HasError() { return []string{} } - sort.Strings(elements) return elements } From bb0e58d25e644f75003768023b2e014dfc0a6748 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 12:26:14 +0200 Subject: [PATCH 56/62] chore: get rid of never true statement --- stackit/internal/services/cdn/distribution/resource.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 2fc5fb9e9..bc7cbe873 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1355,8 +1355,8 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo // Determine if WAF should be entirely excluded to prevent drift. // The API can return an empty string for fully unconfigured backends. - isWafDisabled := (distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED || distribution.Config.Waf.Mode == "") && - (distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE || distribution.Config.Waf.Type == "") + isWafDisabled := (distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED) && + (distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE) var wafVal attr.Value if isWafDisabled && (isImport || utils.IsUndefined(oldConfig.Waf)) { From fd0a5c68a4aaa65e2d41fddb8e7139df8204edb8 Mon Sep 17 00:00:00 2001 From: Politano <47066134+matheuspolitano@users.noreply.github.com> Date: Wed, 6 May 2026 12:26:48 +0200 Subject: [PATCH 57/62] chore: simplify code Co-authored-by: Marcel Jacek <72880145+marceljk@users.noreply.github.com> --- stackit/internal/services/cdn/distribution/resource.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index af80bce3d..a52cf3698 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -944,8 +944,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe configPatch.Waf.LogOnlyRuleCollectionIds = getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds) if !utils.IsUndefined(wafModel.ParanoiaLevel) { - pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString()) - configPatch.Waf.ParanoiaLevel = &pl + cdnConfig.Waf.ParanoiaLevel = new(cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString())) } } From 9225a1ece7c319308be0a9ecec68430e1e36090e Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 12:33:01 +0200 Subject: [PATCH 58/62] chore: fix suggestion --- stackit/internal/services/cdn/distribution/resource.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index e0dad72a4..e749db5ed 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -943,7 +943,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe configPatch.Waf.LogOnlyRuleCollectionIds = getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds) if !utils.IsUndefined(wafModel.ParanoiaLevel) { - cdnConfig.Waf.ParanoiaLevel = new(cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString())) + configPatch.Waf.ParanoiaLevel = new(cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString())) } } @@ -1317,7 +1317,7 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo // Helper to unconditionally map string fields mapWafString := func(apiVal *string) types.String { if apiVal != nil { - return types.StringValue(*apiVal) + return types.StringPointerValue(apiVal) } return types.StringNull() } From 34ba969f8f3507bc782980b3c2cc9629bfc3b338 Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 12:38:29 +0200 Subject: [PATCH 59/62] chore: add small improvement --- stackit/internal/services/cdn/distribution/resource.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index e749db5ed..59527755d 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1335,8 +1335,7 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo var pl *string if distribution.Config.Waf.ParanoiaLevel != nil { - plVal := string(*distribution.Config.Waf.ParanoiaLevel) - pl = &plVal + pl = new(string(*distribution.Config.Waf.ParanoiaLevel)) } wafObjAttrs["paranoia_level"] = mapWafString(pl) wafObjAttrs["allowed_http_versions"] = mapWafSet(distribution.Config.Waf.AllowedHttpVersions) From 37ee74c09f13ccf8087dc0621a92288775afdc8d Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 12:47:23 +0200 Subject: [PATCH 60/62] chore: small adjust --- stackit/internal/conversion/conversion.go | 24 ++++++- .../internal/conversion/conversion_test.go | 62 +++++++++++++++++++ .../services/cdn/distribution/datasource.go | 35 ++++------- .../services/cdn/distribution/resource.go | 46 +++++++------- 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/stackit/internal/conversion/conversion.go b/stackit/internal/conversion/conversion.go index 11bdd5994..18cb69012 100644 --- a/stackit/internal/conversion/conversion.go +++ b/stackit/internal/conversion/conversion.go @@ -10,6 +10,7 @@ import ( "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/terraform-provider-stackit/stackit/internal/core" ) @@ -108,6 +109,16 @@ func Int64ValueToPointer(s basetypes.Int64Value) *int64 { return &value } +// Float32ValueToPointer converts basetypes.Float32Value to a pointer to float32. +// It returns nil if the value is null or unknown. +func Float32ValueToPointer(s basetypes.Float32Value) *float32 { + if s.IsNull() || s.IsUnknown() { + return nil + } + value := s.ValueFloat32() + return &value +} + // Float64ValueToPointer converts basetypes.Float64Value to a pointer to float64. // It returns nil if the value is null or unknown. func Float64ValueToPointer(s basetypes.Float64Value) *float64 { @@ -131,10 +142,21 @@ func BoolValueToPointer(s basetypes.BoolValue) *bool { // StringListToPointer converts basetypes.ListValue to a pointer to a list of strings. // It returns nil if the value is null or unknown. func StringListToPointer(list basetypes.ListValue) (*[]string, error) { + result, err := StringListToSlice(list) + if result == nil { + return nil, err + } + return &result, err +} + +// StringListToSlice converts basetypes.ListValue to a list of strings. +// It returns nil if the value is null or unknown. +func StringListToSlice(list basetypes.ListValue) ([]string, error) { if list.IsNull() || list.IsUnknown() { return nil, nil } + // Instantiate an empty slice to ensure the slice is not nil listStr := []string{} for i, el := range list.Elements() { elStr, ok := el.(types.String) @@ -144,7 +166,7 @@ func StringListToPointer(list basetypes.ListValue) (*[]string, error) { listStr = append(listStr, elStr.ValueString()) } - return &listStr, nil + return listStr, nil } // StringSetToPointer converts basetypes.SetValue to a pointer to a list of strings. diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index c1dfe8763..0ebfccf86 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/google/go-cmp/cmp" @@ -449,3 +450,64 @@ func TestStringSetToSlice(t *testing.T) { }) } } + +func TestStringListToSlice(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in basetypes.ListValue + want []string + wantErr bool + }{ + { + name: "unknown", + in: basetypes.NewListUnknown(types.StringType), + want: nil, + }, + { + name: "null", + in: basetypes.NewListNull(types.StringType), + want: nil, + }, + { + name: "empty list", + in: basetypes.NewListValueMust(types.StringType, []attr.Value{}), + want: []string{}, + }, + { + name: "invalid type", + in: basetypes.NewListValueMust(types.Int64Type, []attr.Value{types.Int64Value(123)}), + wantErr: true, + }, + { + name: "some values", + in: basetypes.NewListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("abc"), + types.StringValue("xyz"), + }, + ), + want: []string{ + "abc", + "xyz", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := StringListToSlice(tt.in) + if tt.wantErr && err == nil { + t.Fatal("expected error") + } + if !tt.wantErr && err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if d := cmp.Diff(got, tt.want); d != "" { + t.Fatalf("no match, diff: %s", d) + } + }) + } +} diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index c962ad620..4d6b5c32c 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -593,17 +593,6 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, return core.DiagsToError(diags) } - // Helper to unconditionally map set fields for WAF - mapWafSet := func(apiList []string) types.Set { - if apiList != nil { - setVal, diags := types.SetValueFrom(ctx, types.StringType, apiList) - if !diags.HasError() { - return setVal - } - } - return types.SetNull(types.StringType) - } - // Map Waf wafVal := types.ObjectNull(wafTypes) if distribution.Config.Waf.Mode != "" { @@ -618,18 +607,18 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, wafObjAttrs["paranoia_level"] = types.StringNull() } - wafObjAttrs["allowed_http_versions"] = mapWafSet(distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafSet(distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafSet(distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) var diagWaf diag.Diagnostics wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 59527755d..3179c8bd0 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1322,34 +1322,23 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo return types.StringNull() } - // Helper to unconditionally map set fields - mapWafSet := func(apiList []string) types.Set { - if apiList != nil { - setVal, diags := types.SetValueFrom(ctx, types.StringType, apiList) - if !diags.HasError() { - return setVal - } - } - return types.SetNull(types.StringType) - } - var pl *string if distribution.Config.Waf.ParanoiaLevel != nil { pl = new(string(*distribution.Config.Waf.ParanoiaLevel)) } wafObjAttrs["paranoia_level"] = mapWafString(pl) - wafObjAttrs["allowed_http_versions"] = mapWafSet(distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafSet(distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafSet(distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mapWafSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) // Determine if WAF should be entirely excluded to prevent drift. // The API can return an empty string for fully unconfigured backends. @@ -1757,3 +1746,14 @@ func getWafSet(ctx context.Context, tfSet basetypes.SetValue) []string { } return elements } + +// Helper to unconditionally map set fields +func mapWafSet(ctx context.Context, apiList []string) types.Set { + if apiList != nil { + setVal, diags := types.SetValueFrom(ctx, types.StringType, apiList) + if !diags.HasError() { + return setVal + } + } + return types.SetNull(types.StringType) +} From 1adc1c89cef145727deff33c0f49e7637663a5ab Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 13:04:41 +0200 Subject: [PATCH 61/62] chore: create function to share between resource and datasource --- .../services/cdn/distribution/datasource.go | 24 ++++++++--------- .../services/cdn/distribution/resource.go | 26 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index 4d6b5c32c..c0b2616bc 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -607,18 +607,18 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, wafObjAttrs["paranoia_level"] = types.StringNull() } - wafObjAttrs["allowed_http_versions"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) var diagWaf diag.Diagnostics wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 3179c8bd0..ab2b5515f 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -1327,18 +1327,18 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo pl = new(string(*distribution.Config.Waf.ParanoiaLevel)) } wafObjAttrs["paranoia_level"] = mapWafString(pl) - wafObjAttrs["allowed_http_versions"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpVersions) - wafObjAttrs["allowed_request_content_types"] = mapWafSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) - wafObjAttrs["allowed_http_methods"] = mapWafSet(ctx, distribution.Config.Waf.AllowedHttpMethods) - wafObjAttrs["enabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleIds) - wafObjAttrs["disabled_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleIds) - wafObjAttrs["log_only_rule_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) - wafObjAttrs["enabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) - wafObjAttrs["disabled_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) - wafObjAttrs["log_only_rule_group_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) - wafObjAttrs["enabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) - wafObjAttrs["disabled_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) - wafObjAttrs["log_only_rule_collection_ids"] = mapWafSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) + wafObjAttrs["allowed_http_versions"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) // Determine if WAF should be entirely excluded to prevent drift. // The API can return an empty string for fully unconfigured backends. @@ -1748,7 +1748,7 @@ func getWafSet(ctx context.Context, tfSet basetypes.SetValue) []string { } // Helper to unconditionally map set fields -func mapWafSet(ctx context.Context, apiList []string) types.Set { +func mustMapStringSet(ctx context.Context, apiList []string) types.Set { if apiList != nil { setVal, diags := types.SetValueFrom(ctx, types.StringType, apiList) if !diags.HasError() { From c4b3b727c4bb19c2b60cbac74166fab7a104ce2a Mon Sep 17 00:00:00 2001 From: Matheus Politano Date: Wed, 6 May 2026 13:07:44 +0200 Subject: [PATCH 62/62] chore: remove ignore state --- stackit/internal/services/cdn/cdn_acc_test.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 3e052ab84..67b4280e6 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -245,14 +245,9 @@ func TestAccCDNDistributionHttp(t *testing.T) { return fmt.Sprintf("%s,%s", testutil.ProjectId, distributionId), nil }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"domains", - "config.waf.allowed_http_methods", - "config.waf.allowed_http_versions", - "config.waf.allowed_request_content_types", - "config.waf.paranoia_level", - }, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domains"}, }, { ResourceName: "stackit_cdn_custom_domain.custom_domain",