diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 799d29c2..a4f28cc7 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -46,6 +46,7 @@ data "stackit_cdn_distribution" "example" { 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)) - `regions` (List of String) The configured regions where content will be hosted @@ -58,6 +59,14 @@ Read-Only: - `type` (String) The configured backend type. Supported values are: `http`. + +### Nested Schema for `config.optimizer` + +Read-Only: + +- `enabled` (Boolean) + + ### Nested Schema for `domains` diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index 48cfbe9c..5a52fd20 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -24,6 +24,9 @@ resource "stackit_cdn_distribution" "example_distribution" { origin_url = "mybackend.onstackit.cloud" } regions = ["EU", "US", "ASIA", "AF", "SA"] + optimizer = { + enabled = true + } } } ``` @@ -54,6 +57,10 @@ Required: - `backend` (Attributes) The configured backend for the distribution (see [below for nested schema](#nestedatt--config--backend)) - `regions` (List of String) The configured regions where content will be hosted +Optional: + +- `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)) + ### Nested Schema for `config.backend` @@ -67,6 +74,14 @@ Optional: - `origin_request_headers` (Map of String) The configured origin request headers for the backend + +### Nested Schema for `config.optimizer` + +Optional: + +- `enabled` (Boolean) + + ### Nested Schema for `domains` diff --git a/examples/resources/stackit_cdn_distribution/resource.tf b/examples/resources/stackit_cdn_distribution/resource.tf index 66cbca97..39565b22 100644 --- a/examples/resources/stackit_cdn_distribution/resource.tf +++ b/examples/resources/stackit_cdn_distribution/resource.tf @@ -6,5 +6,8 @@ resource "stackit_cdn_distribution" "example_distribution" { origin_url = "mybackend.onstackit.cloud" } regions = ["EU", "US", "ASIA", "AF", "SA"] + optimizer = { + enabled = true + } } } diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 6d3b021e..43d24520 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -39,6 +39,9 @@ func configResources(regions string) string { origin_url = "%s" } regions = [%s] + optimizer = { + enabled = true + } } } @@ -108,6 +111,7 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), ), @@ -187,6 +191,7 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.1", "US"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), 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_custom_domain.custom_domain", "status", "ACTIVE"), @@ -212,6 +217,7 @@ func TestAccCDNDistributionResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.2", "ASIA"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index d692f3e3..09afc4ff 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -148,7 +148,17 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe Computed: true, Description: schemaDescriptions["config_regions"], ElementType: types.StringType, - }}, + }, + "optimizer": schema.SingleNestedAttribute{ + Description: schemaDescriptions["config_optimizer"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Computed: true, + }, + }, + }, + }, }, }, } diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 22e4af62..d1cc4e33 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -12,8 +12,10 @@ import ( cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -52,6 +54,7 @@ var schemaDescriptions = map[string]string{ "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 for the distribution", "config_backend_origin_request_headers": "The configured origin request headers for the backend", "domain_name": "The name of the domain", @@ -73,10 +76,13 @@ type Model struct { } 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 + Backend backend `tfsdk:"backend"` // The backend associated with the distribution + Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached + Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration +} +type optimizerConfig struct { + Enabled types.Bool `tfsdk:"enabled"` } - type backend struct { Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" backend is supported OriginURL string `tfsdk:"origin_url"` // The origin URL of the backend @@ -86,6 +92,13 @@ type backend struct { var configTypes = map[string]attr.Type{ "backend": types.ObjectType{AttrTypes: backendTypes}, "regions": types.ListType{ElemType: types.StringType}, + "optimizer": types.ObjectType{ + AttrTypes: optimizerTypes, + }, +} + +var optimizerTypes = map[string]attr.Type{ + "enabled": types.BoolType, } var backendTypes = map[string]attr.Type{ @@ -206,6 +219,20 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Required: true, Description: schemaDescriptions["config"], Attributes: map[string]schema.Attribute{ + "optimizer": schema.SingleNestedAttribute{ + Description: schemaDescriptions["config_optimizer"], + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + }, + }, + Validators: []validator.Object{ + objectvalidator.AlsoRequires(path.MatchRelative().AtName("enabled")), + }, + }, "backend": schema.SingleNestedAttribute{ Required: true, Description: schemaDescriptions["config_backend"], @@ -230,7 +257,8 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques Required: true, Description: schemaDescriptions["config_regions"], ElementType: types.StringType, - }}, + }, + }, }, }, } @@ -349,17 +377,36 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } regions = append(regions, *regionEnum) } - _, err := r.client.PatchDistribution(ctx, projectId, distributionId).PatchDistributionPayload(cdn.PatchDistributionPayload{ - Config: &cdn.ConfigPatch{ - Backend: &cdn.ConfigPatchBackend{ - HttpBackendPatch: &cdn.HttpBackendPatch{ - OriginRequestHeaders: configModel.Backend.OriginRequestHeaders, - OriginUrl: &configModel.Backend.OriginURL, - Type: &configModel.Backend.Type, - }, + + configPatch := &cdn.ConfigPatch{ + Backend: &cdn.ConfigPatchBackend{ + HttpBackendPatch: &cdn.HttpBackendPatch{ + OriginRequestHeaders: configModel.Backend.OriginRequestHeaders, + OriginUrl: &configModel.Backend.OriginURL, + Type: &configModel.Backend.Type, }, - Regions: ®ions, }, + Regions: ®ions, + } + + if !utils.IsUndefined(configModel.Optimizer) { + var optimizerModel optimizerConfig + + diags = configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping optimizer config") + return + } + + optimizer := cdn.NewOptimizerPatch() + if !utils.IsUndefined(optimizerModel.Enabled) { + optimizer.SetEnabled(optimizerModel.Enabled.ValueBool()) + } + configPatch.Optimizer = optimizer + } + + _, err := r.client.PatchDistribution(ctx, projectId, distributionId).PatchDistributionPayload(cdn.PatchDistributionPayload{ + Config: configPatch, IntentId: cdn.PtrString(uuid.NewString()), }).Execute() if err != nil { @@ -495,9 +542,24 @@ func mapFields(distribution *cdn.Distribution, model *Model) error { if diags.HasError() { return core.DiagsToError(diags) } + + optimizerVal := types.ObjectNull(optimizerTypes) + if o := distribution.Config.Optimizer; o != nil { + optimizerEnabled, ok := o.GetEnabledOk() + if ok { + var diags diag.Diagnostics + optimizerVal, diags = types.ObjectValue(optimizerTypes, map[string]attr.Value{ + "enabled": types.BoolValue(optimizerEnabled), + }) + if diags.HasError() { + return core.DiagsToError(diags) + } + } + } cfg, diags := types.ObjectValue(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": modelRegions, + "backend": backend, + "regions": modelRegions, + "optimizer": optimizerVal, }) if diags.HasError() { return core.DiagsToError(diags) @@ -553,11 +615,17 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution if err != nil { return nil, err } + var optimizer *cdn.Optimizer + if cfg.Optimizer != nil { + optimizer = cdn.NewOptimizer(cfg.Optimizer.GetEnabled()) + } + payload := &cdn.CreateDistributionPayload{ IntentId: cdn.PtrString(uuid.NewString()), OriginUrl: cfg.Backend.HttpBackend.OriginUrl, Regions: cfg.Regions, OriginRequestHeaders: cfg.Backend.HttpBackend.OriginRequestHeaders, + Optimizer: optimizer, } return payload, nil @@ -593,7 +661,8 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { originRequestHeaders[k] = v } } - return &cdn.Config{ + + cdnConfig := &cdn.Config{ Backend: &cdn.ConfigBackend{ HttpBackend: &cdn.HttpBackend{ OriginRequestHeaders: &originRequestHeaders, @@ -602,5 +671,19 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { }, }, Regions: ®ions, - }, nil + } + + if !utils.IsUndefined(configModel.Optimizer) { + var optimizerModel optimizerConfig + diags := configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + + if !utils.IsUndefined(optimizerModel.Enabled) { + cdnConfig.Optimizer = cdn.NewOptimizer(optimizerModel.Enabled.ValueBool()) + } + } + + return cdnConfig, nil } diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index f3633ef9..970c7535 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -24,9 +24,13 @@ func TestToCreatePayload(t *testing.T) { }) regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} regionsFixture := types.ListValueMust(types.StringType, regions) + optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ + "enabled": types.BoolValue(true), + }) config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), }) modelFixture := func(mods ...func(*Model)) *Model { model := &Model{ @@ -56,6 +60,25 @@ func TestToCreatePayload(t *testing.T) { }, IsValid: true, }, + "happy_path_with_optimizer": { + Input: modelFixture(func(m *Model) { + m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": optimizer, + }) + }), + Expected: &cdn.CreateDistributionPayload{ + OriginRequestHeaders: &map[string]string{ + "testHeader0": "testHeaderValue0", + "testHeader1": "testHeaderValue1", + }, + OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), + Regions: &[]cdn.Region{"EU", "US"}, + Optimizer: cdn.NewOptimizer(true), + }, + IsValid: true, + }, "sad_path_model_nil": { Input: nil, Expected: nil, @@ -104,9 +127,11 @@ func TestConvertConfig(t *testing.T) { }) regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} regionsFixture := types.ListValueMust(types.StringType, regions) + optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{"enabled": types.BoolValue(true)}) config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), }) modelFixture := func(mods ...func(*Model)) *Model { model := &Model{ @@ -141,6 +166,30 @@ func TestConvertConfig(t *testing.T) { }, IsValid: true, }, + "happy_path_with_optimizer": { + Input: modelFixture(func(m *Model) { + m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": optimizer, + }) + }), + 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"), + }, + }, + Regions: &[]cdn.Region{"EU", "US"}, + Optimizer: cdn.NewOptimizer(true), + }, + IsValid: true, + }, "sad_path_model_nil": { Input: nil, Expected: nil, @@ -188,10 +237,15 @@ func TestMapFields(t *testing.T) { }) regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} regionsFixture := types.ListValueMust(types.StringType, regions) - config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, + optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ + "enabled": types.BoolValue(true), }) + config := types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), + }) + emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{}) managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{ "name": types.StringValue("test.stackit-cdn.com"), @@ -219,17 +273,19 @@ func TestMapFields(t *testing.T) { } distributionFixture := func(mods ...func(*cdn.Distribution)) *cdn.Distribution { distribution := &cdn.Distribution{ - Config: &cdn.Config{Backend: &cdn.ConfigBackend{ - HttpBackend: &cdn.HttpBackend{ - OriginRequestHeaders: &map[string]string{ - "testHeader0": "testHeaderValue0", - "testHeader1": "testHeaderValue1", + Config: &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"), }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Type: cdn.PtrString("http"), }, - }, - Regions: &[]cdn.Region{"EU", "US"}, + Regions: &[]cdn.Region{"EU", "US"}, + Optimizer: nil, }, CreatedAt: &createdAt, Domains: &[]cdn.Domain{ @@ -259,6 +315,21 @@ func TestMapFields(t *testing.T) { Input: distributionFixture(), IsValid: true, }, + "happy_path_with_optimizer": { + Expected: expectedModel(func(m *Model) { + m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": optimizer, + }) + }), + Input: distributionFixture(func(d *cdn.Distribution) { + d.Config.Optimizer = &cdn.Optimizer{ + Enabled: cdn.PtrBool(true), + } + }), + IsValid: true, + }, "happy_path_status_error": { Expected: expectedModel(func(m *Model) { m.Status = types.StringValue("ERROR")