diff --git a/docs/data-sources/alb_waf_configuration.md b/docs/data-sources/alb_waf_configuration.md new file mode 100644 index 000000000..200a23525 --- /dev/null +++ b/docs/data-sources/alb_waf_configuration.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_alb_waf_configuration Data Source - stackit" +subcategory: "" +description: |- + albwaf resource schema. +--- + +# stackit_alb_waf_configuration (Data Source) + +albwaf resource schema. + +## Example Usage + +```terraform +data "stackit_alb_waf_configuration" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-waf-configuration" +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the WAF Configuration. +- `project_id` (String) STACKIT project ID to which the WAF Configuration is associated. + +### Optional + +- `region` (String) The resource region (e.g. eu01). If not defined, the provider region is used. + +### Read-Only + +- `custom_rule_group_name` (String) Name of the custom rule group for this WAF Configuration. +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`name`". +- `labels` (Map of String) User-defined metadata as key-value pairs. Should not exceed 64 entries. +- `managed_rule_set_name` (String) Name of the managed rule set configuration for this WAF Configuration. diff --git a/docs/data-sources/alb_waf_managed_rule_set.md b/docs/data-sources/alb_waf_managed_rule_set.md index 1f335dabf..c14a428bb 100644 --- a/docs/data-sources/alb_waf_managed_rule_set.md +++ b/docs/data-sources/alb_waf_managed_rule_set.md @@ -39,7 +39,6 @@ data "stackit_alb_waf_managed_rule_set" "example" { - `groups` (Attributes Map) Inventory of all available Managed Rule Set groups and their current configuration. (see [below for nested schema](#nestedatt--groups)) - `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`name`". - `type` (String) Type of the Managed Rule Set. -- `usage` (Attributes) Managed Rule Set usage (see [below for nested schema](#nestedatt--usage)) - `version` (String) Managed Rule Set version. @@ -59,13 +58,3 @@ Read-Only: - `description` (String) A description of what this rule does. - `mode` (String) The current mode of the rule. - `severity` (String) Impact level. - - - - -### Nested Schema for `usage` - -Read-Only: - -- `count` (Number) Number of WAFs using this Managed Rule Set. -- `items` (List of String) List of WAFs that use this Managed Rule Set. diff --git a/docs/resources/alb_waf_configuration.md b/docs/resources/alb_waf_configuration.md new file mode 100644 index 000000000..520841338 --- /dev/null +++ b/docs/resources/alb_waf_configuration.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_alb_waf_configuration Resource - stackit" +subcategory: "" +description: |- + albwaf resource schema. +--- + +# stackit_alb_waf_configuration (Resource) + +albwaf resource schema. + +## Example Usage + +```terraform +resource "stackit_alb_waf_configuration" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-waf-configuration" +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the WAF Configuration. +- `project_id` (String) STACKIT project ID to which the WAF Configuration is associated. + +### Optional + +- `custom_rule_group_name` (String) Name of the custom rule group for this WAF Configuration. +- `labels` (Map of String) User-defined metadata as key-value pairs. Should not exceed 64 entries. +- `managed_rule_set_name` (String) Name of the managed rule set configuration for this WAF Configuration. +- `region` (String) The resource region (e.g. eu01). If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`name`". diff --git a/docs/resources/alb_waf_managed_rule_set.md b/docs/resources/alb_waf_managed_rule_set.md index eeb2c93f3..390e30ded 100644 --- a/docs/resources/alb_waf_managed_rule_set.md +++ b/docs/resources/alb_waf_managed_rule_set.md @@ -40,7 +40,6 @@ resource "stackit_alb_waf_managed_rule_set" "example" { - `groups` (Attributes Map) Inventory of all available Managed Rule Set groups and their current configuration. (see [below for nested schema](#nestedatt--groups)) - `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`name`". -- `usage` (Attributes) Managed Rule Set usage (see [below for nested schema](#nestedatt--usage)) - `version` (String) Managed Rule Set version. @@ -60,13 +59,3 @@ Read-Only: - `description` (String) A description of what this rule does. - `mode` (String) The current mode of the rule. - `severity` (String) Impact level. - - - - -### Nested Schema for `usage` - -Read-Only: - -- `count` (Number) Number of WAFs using this Managed Rule Set. -- `items` (List of String) List of WAFs that use this Managed Rule Set. diff --git a/examples/data-sources/stackit_alb_waf_configuration/data-source.tf b/examples/data-sources/stackit_alb_waf_configuration/data-source.tf new file mode 100644 index 000000000..c044ffa5e --- /dev/null +++ b/examples/data-sources/stackit_alb_waf_configuration/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_alb_waf_configuration" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-waf-configuration" +} + diff --git a/examples/resources/stackit_alb_waf_configuration/resource.tf b/examples/resources/stackit_alb_waf_configuration/resource.tf new file mode 100644 index 000000000..027c57e02 --- /dev/null +++ b/examples/resources/stackit_alb_waf_configuration/resource.tf @@ -0,0 +1,5 @@ +resource "stackit_alb_waf_configuration" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-waf-configuration" +} + diff --git a/go.mod b/go.mod index e78461d68..201178ce2 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/terraform-plugin-testing v1.16.0 github.com/stackitcloud/stackit-sdk-go/core v0.26.0 github.com/stackitcloud/stackit-sdk-go/services/alb v0.16.0 - github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.10.0 + github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.11.0 github.com/stackitcloud/stackit-sdk-go/services/cdn v1.19.0 github.com/stackitcloud/stackit-sdk-go/services/certificates v1.9.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.21.0 diff --git a/go.sum b/go.sum index be2ed0801..f0b0d8887 100644 --- a/go.sum +++ b/go.sum @@ -672,8 +672,8 @@ github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10 github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= github.com/stackitcloud/stackit-sdk-go/services/alb v0.16.0 h1:WoWlHdzISGXPEaJOYt6HP5F9M5nbyCJL6VqRJZIaOQs= github.com/stackitcloud/stackit-sdk-go/services/alb v0.16.0/go.mod h1:eK6oRB5Tmpt6KbXQ4UYBGg2LgW5bPtVoncL9E8JSRww= -github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.10.0 h1:0WsTSSZ0LjNpM3E1d3MgkBXmzMQThVQ7IuXhL2w4EyM= -github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.10.0/go.mod h1:4M9G1I64kZwlXO32ZoIpt0GAN4SpZ1SYerwCVVIBGoE= +github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.11.0 h1:ejTZTnGKFUWs9Ch9U30Jd+tpDA/SnHuSF9DpfD6w+To= +github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.11.0/go.mod h1:4M9G1I64kZwlXO32ZoIpt0GAN4SpZ1SYerwCVVIBGoE= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.2 h1:b7WJ/vwxlVmNNX91kI3obqGcuoPAyaCbDL5aCMQ/sNg= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.2/go.mod h1:T/JF25XGJ3GqER/1L2N//DgY8x5tY7gA3N+/0nvmOWY= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.19.0 h1:k+KJ4gp9awhJMY5y55vDqRSr6G/S9+8haTNILGbgH9s= diff --git a/stackit/internal/services/albwaf/albwaf_acc_test.go b/stackit/internal/services/albwaf/albwaf_acc_test.go index b8e539850..d53fb8979 100644 --- a/stackit/internal/services/albwaf/albwaf_acc_test.go +++ b/stackit/internal/services/albwaf/albwaf_acc_test.go @@ -24,6 +24,12 @@ import ( var ( //go:embed testdata/managed-rule-set.tf managedRuleSetConfig string + + //go:embed testdata/resource-max.tf + wafMaxConfig string + + //go:embed testdata/resource-min.tf + wafMinConfig string ) var testManagedRuleSet = config.Variables{ @@ -39,6 +45,35 @@ var testManagedRuleSetUpdated = func() config.Variables { return updatedConfig } +var testConfigVarsMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "waf_configuration_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "rule_set_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "type": config.StringVariable("TYPE_OWASP_CRS"), + "waf_configuration_label": config.StringVariable("some-label"), +} + +var testConfigVarsMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigVarsMax) + updatedConfig["waf_configuration_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["waf_configuration_name"]))) + updatedConfig["rule_set_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["rule_set_name"]))) + updatedConfig["waf_configuration_label"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["waf_configuration_label"]))) + return updatedConfig +} + +var testConfigVarsMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "waf_configuration_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), +} + +var testConfigVarsMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigVarsMin) + updatedConfig["waf_configuration_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["waf_configuration_name"]))) + return updatedConfig +} + func TestAccManagedRuleSet(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, @@ -53,8 +88,6 @@ func TestAccManagedRuleSet(t *testing.T) { resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "region", testutil.Region), resource.TestCheckResourceAttrSet("stackit_alb_waf_managed_rule_set.managed_rule_set", "id"), resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testManagedRuleSet["name"])), - - resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "usage.count", "0"), ), }, // Data source @@ -79,8 +112,6 @@ func TestAccManagedRuleSet(t *testing.T) { "stackit_alb_waf_managed_rule_set.managed_rule_set", "id", ), resource.TestCheckResourceAttr("data.stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testManagedRuleSet["name"])), - - resource.TestCheckResourceAttr("data.stackit_alb_waf_managed_rule_set.managed_rule_set", "usage.count", "0"), ), }, // Import @@ -115,8 +146,183 @@ func TestAccManagedRuleSet(t *testing.T) { resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "region", testutil.Region), resource.TestCheckResourceAttrSet("stackit_alb_waf_managed_rule_set.managed_rule_set", "id"), resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testManagedRuleSetUpdated()["name"])), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccWafConfigurationMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigVarsMin, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), wafMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_configuration.waf_instance", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["waf_configuration_name"])), + ), + }, + // Data source + { + ConfigVariables: testConfigVarsMin, + Config: fmt.Sprintf(` + %s + %s + data "stackit_alb_waf_configuration" "waf" { + project_id = stackit_alb_waf_configuration.waf_instance.project_id + name = stackit_alb_waf_configuration.waf_instance.name + } + `, + testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), wafMinConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_alb_waf_configuration.waf", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_alb_waf_configuration.waf", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_configuration.waf", "id", + "stackit_alb_waf_configuration.waf_instance", "id", + ), + resource.TestCheckResourceAttr("data.stackit_alb_waf_configuration.waf", "name", testutil.ConvertConfigVariable(testConfigVarsMin["waf_configuration_name"])), + ), + }, + // Import + { + ConfigVariables: testConfigVarsMin, + ResourceName: "stackit_alb_waf_configuration.waf_instance", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_alb_waf_configuration.waf_instance"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_alb_waf_configuration.waf_instance") + } + name, ok := r.Primary.Attributes["name"] + if !ok { + return "", fmt.Errorf("couldn't find attribute name") + } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, name), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigVarsMinUpdated(), + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), wafMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_alb_waf_configuration.waf_instance", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_configuration.waf_instance", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "name", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["waf_configuration_name"])), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccWafConfigurationMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigVarsMax, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), wafMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testConfigVarsMax["rule_set_name"])), + + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_configuration.waf_instance", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["waf_configuration_name"])), + resource.TestCheckResourceAttrPair("stackit_alb_waf_configuration.waf_instance", "managed_rule_set_name", "stackit_alb_waf_managed_rule_set.managed_rule_set", "name"), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["waf_configuration_label"])), + ), + }, + // Data source + { + ConfigVariables: testConfigVarsMax, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_alb_waf_managed_rule_set" "managed_rule_set" { + project_id = stackit_alb_waf_managed_rule_set.managed_rule_set.project_id + name = stackit_alb_waf_managed_rule_set.managed_rule_set.name + } + + data "stackit_alb_waf_configuration" "waf" { + project_id = stackit_alb_waf_configuration.waf_instance.project_id + name = stackit_alb_waf_configuration.waf_instance.name + } + `, + testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), wafMaxConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_managed_rule_set.managed_rule_set", "id", + "stackit_alb_waf_managed_rule_set.managed_rule_set", "id", + ), + resource.TestCheckResourceAttr("data.stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testConfigVarsMax["rule_set_name"])), + + resource.TestCheckResourceAttr("data.stackit_alb_waf_configuration.waf", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_alb_waf_configuration.waf", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_configuration.waf", "id", + "stackit_alb_waf_configuration.waf_instance", "id", + ), + resource.TestCheckResourceAttr("data.stackit_alb_waf_configuration.waf", "name", testutil.ConvertConfigVariable(testConfigVarsMax["waf_configuration_name"])), + resource.TestCheckResourceAttrPair("data.stackit_alb_waf_configuration.waf", "managed_rule_set_name", "data.stackit_alb_waf_managed_rule_set.managed_rule_set", "name"), + resource.TestCheckResourceAttr("data.stackit_alb_waf_configuration.waf", "labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["waf_configuration_label"])), + ), + }, + // Import + { + ConfigVariables: testConfigVarsMax, + ResourceName: "stackit_alb_waf_configuration.waf_instance", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_alb_waf_configuration.waf_instance"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_alb_waf_configuration.waf_instance") + } + name, ok := r.Primary.Attributes["name"] + if !ok { + return "", fmt.Errorf("couldn't find attribute name") + } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, name), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigVarsMaxUpdated(), + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), wafMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_alb_waf_configuration.waf_instance", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_configuration.waf_instance", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "name", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["waf_configuration_name"])), + resource.TestCheckResourceAttr("stackit_alb_waf_configuration.waf_instance", "labels.label1", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["waf_configuration_label"])), - resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "usage.count", "0"), + resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["rule_set_name"])), ), }, // Deletion is done by the framework implicitly diff --git a/stackit/internal/services/albwaf/managed_rule_set/datasource.go b/stackit/internal/services/albwaf/managed_rule_set/datasource.go index b802907a4..f53205ae2 100644 --- a/stackit/internal/services/albwaf/managed_rule_set/datasource.go +++ b/stackit/internal/services/albwaf/managed_rule_set/datasource.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" @@ -100,21 +99,6 @@ func (d *managedRuleSetDataSource) Schema(_ context.Context, _ datasource.Schema Description: descriptions["version"], Computed: true, }, - "usage": schema.SingleNestedAttribute{ - Description: descriptions["usage"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "count": schema.Int32Attribute{ - Description: descriptions["usage_count"], - Computed: true, - }, - "items": schema.ListAttribute{ - Description: descriptions["usage_items"], - Computed: true, - ElementType: types.StringType, - }, - }, - }, "groups": schema.MapNestedAttribute{ Description: descriptions["groups"], Computed: true, diff --git a/stackit/internal/services/albwaf/managed_rule_set/resource.go b/stackit/internal/services/albwaf/managed_rule_set/resource.go index 0409ec6e8..57cbb6664 100644 --- a/stackit/internal/services/albwaf/managed_rule_set/resource.go +++ b/stackit/internal/services/albwaf/managed_rule_set/resource.go @@ -43,7 +43,6 @@ type Model struct { Name types.String `tfsdk:"name"` Groups types.Map `tfsdk:"groups"` Type types.String `tfsdk:"type"` - Usage types.Object `tfsdk:"usage"` Version types.String `tfsdk:"version"` } @@ -73,16 +72,6 @@ var ruleType = map[string]attr.Type{ "severity": types.StringType, } -type UsageModel struct { - Count types.Int32 `tfsdk:"count"` - Items types.List `tfsdk:"items"` -} - -var usageType = map[string]attr.Type{ - "count": types.Int32Type, - "items": types.ListType{ElemType: types.StringType}, -} - type managedRuleSetResource struct { client *albWaf.APIClient providerData core.ProviderData @@ -124,9 +113,6 @@ var descriptions = map[string]string{ "name": "Managed Rule Set configuration name.", "type": "Type of the Managed Rule Set.", "version": "Managed Rule Set version.", - "usage": "Managed Rule Set usage", - "usage_count": "Number of WAFs using this Managed Rule Set.", - "usage_items": "List of WAFs that use this Managed Rule Set.", "groups": "Inventory of all available Managed Rule Set groups and their current configuration.", "group_description": "A description of what this group covers.", "group_name": "The name for the rule group.", @@ -190,21 +176,6 @@ func (r *managedRuleSetResource) Schema(_ context.Context, _ resource.SchemaRequ Description: descriptions["version"], Computed: true, }, - "usage": schema.SingleNestedAttribute{ - Description: descriptions["usage"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "count": schema.Int32Attribute{ - Description: descriptions["usage_count"], - Computed: true, - }, - "items": schema.ListAttribute{ - Description: descriptions["usage_items"], - Computed: true, - ElementType: types.StringType, - }, - }, - }, "groups": schema.MapNestedAttribute{ Description: descriptions["groups"], Computed: true, @@ -266,6 +237,27 @@ func (r *managedRuleSetResource) ModifyPlan(ctx context.Context, req resource.Mo return } + // Warn the user if the name is changing, as this triggers a replacement. + // Deletion of the old resource will fail if another resource (e.g. stackit_alb_waf_configuration) + // still references this managed rule set. + if !req.State.Raw.IsNull() { + var stateModel Model + resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...) + if !resp.Diagnostics.HasError() && !stateModel.Name.IsNull() && !stateModel.Name.IsUnknown() { + if !planModel.Name.Equal(stateModel.Name) { + resp.Diagnostics.AddWarning( + "Managed Rule Set name change requires resource replacement", + fmt.Sprintf( + "Changing the \"name\" attribute from %q to %q will destroy and recreate this resource. "+ + "If another resource (e.g. \"stackit_alb_waf_configuration\") references this managed rule set "+ + "by name, the replacement will fail. Remove or update that dependency before applying this change.", + stateModel.Name.ValueString(), planModel.Name.ValueString(), + ), + ) + } + } + } + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) if resp.Diagnostics.HasError() { return @@ -432,8 +424,8 @@ func toCreatePayload(_ context.Context, model *Model) (*albWaf.CreateManagedRule } payload := &albWaf.CreateManagedRuleSetPayload{ - Name: model.Name.ValueStringPointer(), - Type: new(albWaf.MRSType(model.Type.ValueString())), + Name: model.Name.ValueString(), + Type: albWaf.MRSType(model.Type.ValueString()), } return payload, nil @@ -498,23 +490,5 @@ func mapFields(ctx context.Context, managedRuleSet *albWaf.GetManagedRuleSetResp return fmt.Errorf("mapping groups: %w", core.DiagsToError(diags)) } - if usage, ok := managedRuleSet.GetUsageOk(); ok { - usageModel := UsageModel{ - Count: types.Int32PointerValue(usage.Count), - } - - usageModel.Items, diags = types.ListValueFrom(ctx, types.StringType, usage.GetItems()) - if diags.HasError() { - return fmt.Errorf("creating usage object: %w", core.DiagsToError(diags)) - } - - model.Usage, diags = types.ObjectValueFrom(ctx, usageType, usageModel) - if diags.HasError() { - return fmt.Errorf("creating usage object: %w", core.DiagsToError(diags)) - } - } else { - model.Usage = types.ObjectNull(usageType) - } - return nil } diff --git a/stackit/internal/services/albwaf/managed_rule_set/resource_test.go b/stackit/internal/services/albwaf/managed_rule_set/resource_test.go index 9b7bc9548..0158f48f6 100644 --- a/stackit/internal/services/albwaf/managed_rule_set/resource_test.go +++ b/stackit/internal/services/albwaf/managed_rule_set/resource_test.go @@ -36,8 +36,8 @@ func TestToCreatePayload(t *testing.T) { Type: types.StringValue(string(albWaf.MRSTYPE_TYPE_OWASP_CRS)), }, expected: &albWaf.CreateManagedRuleSetPayload{ - Name: testName.ValueStringPointer(), - Type: new(albWaf.MRSTYPE_TYPE_OWASP_CRS), + Name: testName.ValueString(), + Type: albWaf.MRSTYPE_TYPE_OWASP_CRS, }, isValid: true, }, diff --git a/stackit/internal/services/albwaf/testdata/managed-rule-set.tf b/stackit/internal/services/albwaf/testdata/managed-rule-set.tf index de5736aca..348e7bb4d 100644 --- a/stackit/internal/services/albwaf/testdata/managed-rule-set.tf +++ b/stackit/internal/services/albwaf/testdata/managed-rule-set.tf @@ -1,4 +1,3 @@ - variable "project_id" {} variable "type" {} variable "name" {} diff --git a/stackit/internal/services/albwaf/testdata/resource-max.tf b/stackit/internal/services/albwaf/testdata/resource-max.tf new file mode 100644 index 000000000..e3ee71ae6 --- /dev/null +++ b/stackit/internal/services/albwaf/testdata/resource-max.tf @@ -0,0 +1,20 @@ +variable "project_id" {} +variable "waf_configuration_name" {} +variable "rule_set_name" {} +variable "type" {} +variable "waf_configuration_label" {} + +resource "stackit_alb_waf_managed_rule_set" "managed_rule_set" { + project_id = var.project_id + type = var.type + name = var.rule_set_name +} +resource "stackit_alb_waf_configuration" "waf_instance" { + project_id = var.project_id + name = var.waf_configuration_name + managed_rule_set_name = stackit_alb_waf_managed_rule_set.managed_rule_set.name + labels = { + label1 = var.waf_configuration_label + } +} + diff --git a/stackit/internal/services/albwaf/testdata/resource-min.tf b/stackit/internal/services/albwaf/testdata/resource-min.tf new file mode 100644 index 000000000..4d401ce5c --- /dev/null +++ b/stackit/internal/services/albwaf/testdata/resource-min.tf @@ -0,0 +1,8 @@ +variable "project_id" {} +variable "waf_configuration_name" {} + +resource "stackit_alb_waf_configuration" "waf_instance" { + project_id = var.project_id + name = var.waf_configuration_name +} + diff --git a/stackit/internal/services/albwaf/waf_configuration/datasource.go b/stackit/internal/services/albwaf/waf_configuration/datasource.go new file mode 100644 index 000000000..89859f77f --- /dev/null +++ b/stackit/internal/services/albwaf/waf_configuration/datasource.go @@ -0,0 +1,148 @@ +package waf + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + waf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + albwafUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = &wafDatasource{} + _ datasource.DataSourceWithConfigure = &wafDatasource{} +) + +type wafDatasource struct { + client *waf.APIClient + providerData core.ProviderData +} + +func NewWafConfigurationDatasource() datasource.DataSource { + return &wafDatasource{} +} + +func (d *wafDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { // nolint:gocritic // function signature required by Terraform + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_alb_waf_configuration", core.Datasource) + if resp.Diagnostics.HasError() { + return + } + + d.client = albwafUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF client configured") +} + +func (d *wafDatasource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + }, + "region": schema.StringAttribute{ + Description: descriptions["region"], + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + Computed: true, + ElementType: types.StringType, + }, + "managed_rule_set_name": schema.StringAttribute{ + Description: descriptions["managed_rule_set_name"], + Computed: true, + }, + "custom_rule_group_name": schema.StringAttribute{ + Description: descriptions["custom_rule_group_name"], + Computed: true, + }, + }, + } +} + +func (d *wafDatasource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_alb_waf_configuration" +} + +func (d *wafDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + name := model.Name.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", name) + + foundWAF, err := d.client.DefaultAPI.GetWAF(ctx, projectId, region, name).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + utils.LogError(ctx, &resp.Diagnostics, err, "Error reading ALB WAF Configuration:", fmt.Sprintf("Calling API: %v", err), map[int]string{http.StatusNotFound: fmt.Sprintf("ALB WAF Configuration %s not found", name)}) + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Configuration", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, foundWAF, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Configuration", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF Configuration read") +} diff --git a/stackit/internal/services/albwaf/waf_configuration/resource.go b/stackit/internal/services/albwaf/waf_configuration/resource.go new file mode 100644 index 000000000..e6dd622c2 --- /dev/null +++ b/stackit/internal/services/albwaf/waf_configuration/resource.go @@ -0,0 +1,486 @@ +package waf + +import ( + "context" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + waf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + albwafUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/utils" + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &wafResource{} + _ resource.ResourceWithConfigure = &wafResource{} + _ resource.ResourceWithImportState = &wafResource{} + _ resource.ResourceWithModifyPlan = &wafResource{} +) + +type Model struct { + Id types.String `tfsdk:"id"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Name types.String `tfsdk:"name"` + Labels types.Map `tfsdk:"labels"` + ManagedRuleSetName types.String `tfsdk:"managed_rule_set_name"` + CustomRuleGroupName types.String `tfsdk:"custom_rule_group_name"` +} + +func NewWafConfigurationResource() resource.Resource { + return &wafResource{} +} + +type ItemsModel struct { + ListenerNames types.Int32 `tfsdk:"listener_names"` + LoadBalancerName types.String `tfsdk:"load_balancer_name"` +} + +type wafResource struct { + client *waf.APIClient + providerData core.ProviderData +} + +func (r *wafResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_alb_waf_configuration" +} + +// Use the modifier to set the effective region in the current plan. +func (r *wafResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + // skip initial empty configuration to avoid follow-up errors + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + tfutils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *wafResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_alb_waf_configuration", core.Resource) + if resp.Diagnostics.HasError() { + return + } + apiClient := albwafUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "albwaf client configured") +} + +var descriptions = map[string]string{ + "main": "albwaf resource schema.", + "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`name`\".", + "project_id": "STACKIT project ID to which the WAF Configuration is associated.", + "region": "The resource region (e.g. eu01). If not defined, the provider region is used.", + "name": "The name of the WAF Configuration.", + "labels": "User-defined metadata as key-value pairs. Should not exceed 64 entries.", + "managed_rule_set_name": "Name of the managed rule set configuration for this WAF Configuration.", + "custom_rule_group_name": "Name of the custom rule group for this WAF Configuration.", + "count": "Number of listeners using this WAF Configuration.", + "items": "List of Application Load Balancers with their associated listeners that use this WAF Configuration.", + "listener_names": "List of listener names in this Application Load Balancer using this WAF Configuration.", + "load_balancer_name": "The display name of the Application Load Balancer.", +} + +func (r *wafResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: descriptions["main"], + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[0-9a-z](?:(?:[0-9a-z]|-){0,61}[0-9a-z])?$`), + "must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long", + ), + }, + }, + "region": schema.StringAttribute{ + Description: descriptions["region"], + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "labels": schema.MapAttribute{ + Description: descriptions["labels"], + Optional: true, + ElementType: types.StringType, + Validators: []validator.Map{ + mapvalidator.SizeAtMost(64), + }, + }, + "managed_rule_set_name": schema.StringAttribute{ + Description: descriptions["managed_rule_set_name"], + Optional: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[0-9a-z](?:(?:[0-9a-z]|-){0,61}[0-9a-z])?$`), + "must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long", + ), + }, + }, + "custom_rule_group_name": schema.StringAttribute{ + Description: descriptions["custom_rule_group_name"], + Optional: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[0-9a-z](?:(?:[0-9a-z]|-){0,61}[0-9a-z])?$`), + "must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long", + ), + }, + }, + }, + } +} + +func (r *wafResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", model.Name) + + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating ALB WAF Configuration", fmt.Sprint("Creating API payload: %w", err)) + return + } + createResp, err := r.client.DefaultAPI.CreateWAF(ctx, projectId, region).CreateWAFPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating ALB WAF Configuration", fmt.Sprint("Calling API: %w", err)) + return + } + ctx = core.LogResponse(ctx) + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "name": createResp.Name, + }) + if resp.Diagnostics.HasError() { + return + } + + err = mapFields(ctx, createResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating ALB WAF Configuration", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF Configuration created") +} + +func (r *wafResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + name := model.Name.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", name) + + _, err := r.client.DefaultAPI.DeleteWAF(ctx, projectId, region, name).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting ALB WAF Configuration", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + tflog.Info(ctx, "ALB WAF Configuration deleted") +} + +func (r *wafResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + name := model.Name.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + if name == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Configuration", "Name must be defined when reading ALB WAF Configuration") + return + } + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", name) + + response, err := r.client.DefaultAPI.GetWAF(ctx, projectId, region, name).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Configuration", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, response, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Configuration", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF Configuration read") +} + +func (r *wafResource) 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) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + name := model.Name.ValueString() + + if name == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating ALB WAF Configuration", "Name must be defined when updating ALB WAF Configuration") + return + } + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", name) + + payload, err := toUpdatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating ALB WAF Configuration", fmt.Sprint("Creating API payload: %w", err)) + return + } + updateResp, err := r.client.DefaultAPI.UpdateWAF(ctx, projectId, region, name).UpdateWAFPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating ALB WAF Configuration", fmt.Sprint("Calling API: %w", err)) + return + } + ctx = core.LogResponse(ctx) + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "name": updateResp.Name, + }) + if resp.Diagnostics.HasError() { + return + } + + err = mapFields(ctx, updateResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating ALB WAF Configuration", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF Configuration created") +} + +func (r *wafResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing ALB WAF Configuration", + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[name] Got: %q", req.ID), + ) + return + } + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "name": idParts[2], + }) + tflog.Info(ctx, "ALB WAF Configuration state imported") +} + +func toUpdatePayload(ctx context.Context, model *Model) (*waf.UpdateWAFPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + var labels *map[string]string + if !(model.Labels.IsNull() || model.Labels.IsUnknown()) { + diags := model.Labels.ElementsAs(ctx, &labels, false) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + } + return &waf.UpdateWAFPayload{ + CustomRuleGroupName: model.CustomRuleGroupName.ValueStringPointer(), + ManagedRuleSetName: model.ManagedRuleSetName.ValueStringPointer(), + Labels: labels, + }, nil +} + +func toCreatePayload(ctx context.Context, model *Model) (*waf.CreateWAFPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + var labels *map[string]string + if !(model.Labels.IsNull() || model.Labels.IsUnknown()) { + diags := model.Labels.ElementsAs(ctx, &labels, false) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + } + payload := &waf.CreateWAFPayload{ + Name: model.Name.ValueString(), + CustomRuleGroupName: model.CustomRuleGroupName.ValueStringPointer(), + Labels: labels, + ManagedRuleSetName: model.ManagedRuleSetName.ValueStringPointer(), + } + return payload, nil +} + +func mapFields(ctx context.Context, wafResponse *waf.GetWAFResponse, model *Model, region string) error { + if wafResponse == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + labels, err := tfutils.MapLabels(ctx, wafResponse.Labels, model.Labels) + if err != nil { + return err + } + + var name types.String + if wafResponse.Name != nil { + name = types.StringValue(*wafResponse.Name) + } else { + name = types.StringNull() + } + + var customRuleGroupName types.String + if wafResponse.CustomRuleGroupName != nil { + customRuleGroupName = types.StringValue(*wafResponse.CustomRuleGroupName) + } else { + customRuleGroupName = types.StringNull() + } + + var managedRuleSetName types.String + if wafResponse.ManagedRuleSetName != nil { + managedRuleSetName = types.StringValue(*wafResponse.ManagedRuleSetName) + } else { + managedRuleSetName = types.StringNull() + } + + model.Id = tfutils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.Name.ValueString()) + model.Name = name + model.Region = types.StringValue(region) + model.CustomRuleGroupName = customRuleGroupName + model.Labels = labels + model.ManagedRuleSetName = managedRuleSetName + return nil +} diff --git a/stackit/internal/services/albwaf/waf_configuration/resource_test.go b/stackit/internal/services/albwaf/waf_configuration/resource_test.go new file mode 100644 index 000000000..613edf543 --- /dev/null +++ b/stackit/internal/services/albwaf/waf_configuration/resource_test.go @@ -0,0 +1,154 @@ +package waf + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + waf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" +) + +const ( + testRegion = "eu01" +) + +func Test_mapFieds(t *testing.T) { + fixtureModel := func(mods ...func(*Model)) *Model { + m := Model{ + Id: types.StringValue(fmt.Sprintf("pid,%s,name", testRegion)), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + Name: types.StringValue("name"), + Labels: types.MapNull(types.StringType), + ManagedRuleSetName: types.StringNull(), + CustomRuleGroupName: types.StringNull(), + } + + for _, mod := range mods { + mod(&m) + } + return &m + } + tests := []struct { + name string + input *waf.GetWAFResponse + state *Model + region string + want *Model + wantErr bool + }{ + { + name: "default_values", + input: &waf.GetWAFResponse{ + Name: new("name"), + }, + state: fixtureModel(), + region: testRegion, + want: fixtureModel(), + wantErr: false, + }, + { + name: "simple values", + input: &waf.GetWAFResponse{ + Name: new("name"), + Labels: &map[string]string{"label1": "value1"}, + ManagedRuleSetName: new("managed_rule_set"), + CustomRuleGroupName: new("custom_rule_group"), + }, + state: fixtureModel(), + region: testRegion, + want: fixtureModel( + func(m *Model) { + m.Labels = types.MapValueMust(types.StringType, map[string]attr.Value{ + "label1": types.StringValue("value1"), + }) + m.ManagedRuleSetName = types.StringValue("managed_rule_set") + m.CustomRuleGroupName = types.StringValue("custom_rule_group") + }, + ), + wantErr: false, + }, + { + name: "fails when model is nil", + state: nil, + wantErr: true, + }, + { + name: "fails when input is nil", + input: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotErr := mapFields(t.Context(), tt.input, tt.state, tt.region) + + if gotErr != nil { + if !tt.wantErr { + t.Errorf("mapFieds() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("mapFieds() succeeded unexpectedly") + } + if !tt.wantErr { + diff := cmp.Diff(tt.state, tt.want) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func Test_toCreatePayload(t *testing.T) { + tests := []struct { + name string + model *Model + want *waf.CreateWAFPayload + wantErr bool + }{ + { + name: "basic values", + model: &Model{ + ManagedRuleSetName: types.StringValue("example"), + CustomRuleGroupName: types.StringValue("example group name"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "label1": types.StringValue("value1"), + }), + }, + want: &waf.CreateWAFPayload{ + ManagedRuleSetName: new("example"), + CustomRuleGroupName: new("example group name"), + Labels: &map[string]string{"label1": "value1"}, + }, + wantErr: false, + }, + { + name: "fails when model is nil", + model: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := toCreatePayload(t.Context(), tt.model) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("toCreatePayload() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("toCreatePayload() succeeded unexpectedly") + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Errorf("Data does not match: %s", diff) + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index 3a88d9dc4..1e0402cb4 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -25,6 +25,7 @@ import ( alb "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/alb/applicationloadbalancer" cert "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albcertificates/certificate" albWafManagedRuleSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/managed_rule_set" + albWaf "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/waf_configuration" customRole "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/customrole" roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments" cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain" @@ -681,6 +682,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource { dataSources := []func() datasource.DataSource{ alb.NewApplicationLoadBalancerDataSource, + albWaf.NewWafConfigurationDatasource, albWafManagedRuleSet.NewManagedRuleSetDataSource, alertGroup.NewAlertGroupDataSource, cdn.NewDistributionDataSource, @@ -795,6 +797,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func (p *Provider) Resources(_ context.Context) []func() resource.Resource { resources := []func() resource.Resource{ alb.NewApplicationLoadBalancerResource, + albWaf.NewWafConfigurationResource, albWafManagedRuleSet.NewManagedRuleSetResource, alertGroup.NewAlertGroupResource, cdn.NewDistributionResource,