diff --git a/stackit/internal/services/albwaf/custom_rule_group/bool.go b/stackit/internal/services/albwaf/custom_rule_group/bool.go deleted file mode 100644 index 1984c47bd..000000000 --- a/stackit/internal/services/albwaf/custom_rule_group/bool.go +++ /dev/null @@ -1,61 +0,0 @@ -package custom_rule_group - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -// TODO: will be moved to validators within STACKITTPR-786 - -// OnlyAllowedIfBoolEqualsValidator prevents that this string attribute is set if a target bool does not equal the specified value. -type OnlyAllowedIfBoolEqualsValidator struct { - Target path.Expression - Value bool -} - -// Ensure the validator implements the String validator interface -var _ validator.String = OnlyAllowedIfBoolEqualsValidator{} - -func (v OnlyAllowedIfBoolEqualsValidator) Description(_ context.Context) string { - return "The attribute can only be set if the boolean is set to the provided value." -} - -func (v OnlyAllowedIfBoolEqualsValidator) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -func (v OnlyAllowedIfBoolEqualsValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { // nolint:gocritic // function signature required by Terraform - expression := req.PathExpression.Merge(v.Target) - - matchedPaths, diags := req.Config.PathMatches(ctx, expression) - resp.Diagnostics.Append(diags...) - - for _, target := range matchedPaths { - var targetBool types.Bool - diags := req.Config.GetAttribute(ctx, target, &targetBool) - resp.Diagnostics.Append(diags...) - - if resp.Diagnostics.HasError() || targetBool.IsUnknown() { - return - } - - if targetBool.ValueBool() != v.Value && !req.ConfigValue.IsNull() { - resp.Diagnostics.AddAttributeError( - req.Path, - "Attribute can not be set", - fmt.Sprintf("This attribute can only be configured when %q is set to %t.", target.String(), v.Value), - ) - } - } -} - -func OnlyAllowedIfBoolEquals(target path.Expression, value bool) validator.String { - return OnlyAllowedIfBoolEqualsValidator{ - Target: target, - Value: value, - } -} diff --git a/stackit/internal/services/albwaf/custom_rule_group/bool_test.go b/stackit/internal/services/albwaf/custom_rule_group/bool_test.go deleted file mode 100644 index a9d2910c7..000000000 --- a/stackit/internal/services/albwaf/custom_rule_group/bool_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package custom_rule_group - -import ( - "context" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -func TestOnlyIfBoolValidator(t *testing.T) { - tests := []struct { - description string - target types.Bool - expectedValue bool - isValid bool - }{ - { - description: "target true, expect true", - target: types.BoolValue(true), - expectedValue: true, - isValid: true, - }, - { - description: "target false, expect true", - target: types.BoolValue(false), - expectedValue: true, - isValid: false, - }, - { - description: "target false, expect false", - target: types.BoolValue(false), - expectedValue: false, - isValid: true, - }, - { - description: "target true, expect false", - target: types.BoolValue(true), - expectedValue: false, - isValid: false, - }, - { - description: "target unknown, expect true", - target: types.BoolUnknown(), - expectedValue: true, - isValid: true, - }, - { - description: "target unknown, expect false", - target: types.BoolUnknown(), - expectedValue: false, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - ctx := context.Background() - - boolVal, err := tt.target.ToTerraformValue(ctx) - if err != nil { - t.Fatalf("Failed to convert bool to tftypes.Value: %s", err) - } - - objType := tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "target_bool": tftypes.Bool, - }, - } - rawConfig := tftypes.NewValue(objType, map[string]tftypes.Value{ - "target_bool": boolVal, - }) - - req := validator.StringRequest{ - Path: path.Root("my_string"), - PathExpression: path.MatchRoot("my_string"), - ConfigValue: types.StringValue("example_string"), - Config: tfsdk.Config{ - Raw: rawConfig, - Schema: schema.Schema{ - Attributes: map[string]schema.Attribute{ - "target_bool": schema.BoolAttribute{}, - }, - }, - }, - } - - resp := &validator.StringResponse{} - - OnlyAllowedIfBoolEquals(path.MatchRoot("target_bool"), tt.expectedValue).ValidateString(ctx, req, resp) - - if tt.isValid { - if resp.Diagnostics.HasError() { - t.Fatalf("did not expect validation error, got: %v", resp.Diagnostics) - } - } else { - hasExpectedError := false - - for _, diag := range resp.Diagnostics { - if diag.Summary() == "Attribute can not be set" { - hasExpectedError = true - } else { - t.Fatalf("expected validation error, got %q", diag.Summary()) - } - } - - if !hasExpectedError { - t.Fatalf("expected 'Attribute can not be set' error, got: %v", resp.Diagnostics) - } - } - }) - } -} diff --git a/stackit/internal/services/albwaf/custom_rule_group/resource.go b/stackit/internal/services/albwaf/custom_rule_group/resource.go index 5bd14a708..76de82db0 100644 --- a/stackit/internal/services/albwaf/custom_rule_group/resource.go +++ b/stackit/internal/services/albwaf/custom_rule_group/resource.go @@ -246,7 +246,7 @@ func (r *customRuleGroupResource) Schema(_ context.Context, _ resource.SchemaReq Optional: true, Computed: true, Validators: []validator.String{ - OnlyAllowedIfBoolEquals(path.MatchRelative().AtParent().AtName("log"), true), + validate.OnlyAllowedIfBoolEquals(path.MatchRelative().AtParent().AtName("log"), sdkUtils.Ptr(true)), }, }, "severity": schema.StringAttribute{ diff --git a/stackit/internal/validate/validate.go b/stackit/internal/validate/validate.go index 3ebf658da..1af817bd5 100644 --- a/stackit/internal/validate/validate.go +++ b/stackit/internal/validate/validate.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/teambition/rrule-go" @@ -399,6 +400,42 @@ func IsLowercased() *Validator { } } +// OnlyAllowedIfBoolEquals returns a Validator that prevents this string attribute +// from being set if the target bool attribute does not equal the specified value. +// If value is nil, no validation is performed. +func OnlyAllowedIfBoolEquals(target path.Expression, value *bool) *Validator { + description := "the attribute can only be set if the boolean is set to the provided value" + + return &Validator{ + description: description, + validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + expression := req.PathExpression.Merge(target) + + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + resp.Diagnostics.Append(diags...) + + for _, targetPath := range matchedPaths { + var targetBool types.Bool + diags := req.Config.GetAttribute(ctx, targetPath, &targetBool) + resp.Diagnostics.Append(diags...) + + // nothing to validate against: no expected value given or target not set in the config + if resp.Diagnostics.HasError() || value == nil || targetBool.IsNull() || targetBool.IsUnknown() { + return + } + + if targetBool.ValueBool() != *value { + resp.Diagnostics.AddAttributeError( + req.Path, + "Attribute can not be set", + fmt.Sprintf("This attribute can only be configured when %q is set to %t.", targetPath.String(), *value), + ) + } + } + }, + } +} + // NoLeadingOrTrailingWhitespace returns a Validator that checks if the input string has leading or trailing whitespace. // Examples: // - "example": valid diff --git a/stackit/internal/validate/validate_test.go b/stackit/internal/validate/validate_test.go index 1f0cdced9..9a49066e1 100644 --- a/stackit/internal/validate/validate_test.go +++ b/stackit/internal/validate/validate_test.go @@ -4,11 +4,13 @@ import ( "context" "testing" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/stackitcloud/stackit-sdk-go/core/utils" ) func TestUUID(t *testing.T) { @@ -1085,3 +1087,131 @@ func TestNoLeadingOrtTrailingWhitespace(t *testing.T) { }) } } + +func TestOnlyAllowedIfBoolEquals(t *testing.T) { + tests := []struct { + description string + target types.Bool + expectedValue *bool + isValid bool + }{ + { + description: "target true, expect true", + target: types.BoolValue(true), + expectedValue: utils.Ptr(true), + isValid: true, + }, + { + description: "target false, expect true", + target: types.BoolValue(false), + expectedValue: utils.Ptr(true), + isValid: false, + }, + { + description: "target false, expect false", + target: types.BoolValue(false), + expectedValue: utils.Ptr(false), + isValid: true, + }, + { + description: "target true, expect false", + target: types.BoolValue(true), + expectedValue: utils.Ptr(false), + isValid: false, + }, + { + description: "target unknown, expect true", + target: types.BoolUnknown(), + expectedValue: utils.Ptr(true), + isValid: true, + }, + { + description: "target unknown, expect false", + target: types.BoolUnknown(), + expectedValue: utils.Ptr(false), + isValid: true, + }, + { + description: "target null, expect true", + target: types.BoolNull(), + expectedValue: utils.Ptr(true), + isValid: true, + }, + { + description: "target null, expect false", + target: types.BoolNull(), + expectedValue: utils.Ptr(false), + isValid: true, + }, + { + description: "target true, expect nil", + target: types.BoolValue(true), + expectedValue: nil, + isValid: true, + }, + { + description: "target false, expect nil", + target: types.BoolValue(false), + expectedValue: nil, + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + ctx := context.Background() + + boolVal, err := tt.target.ToTerraformValue(ctx) + if err != nil { + t.Fatalf("Failed to convert bool to tftypes.Value: %s", err) + } + + objType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "target_bool": tftypes.Bool, + }, + } + rawConfig := tftypes.NewValue(objType, map[string]tftypes.Value{ + "target_bool": boolVal, + }) + + req := validator.StringRequest{ + Path: path.Root("my_string"), + PathExpression: path.MatchRoot("my_string"), + ConfigValue: types.StringValue("example_string"), + Config: tfsdk.Config{ + Raw: rawConfig, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "target_bool": schema.BoolAttribute{}, + }, + }, + }, + } + + resp := &validator.StringResponse{} + + OnlyAllowedIfBoolEquals(path.MatchRoot("target_bool"), tt.expectedValue).ValidateString(ctx, req, resp) + + if tt.isValid { + if resp.Diagnostics.HasError() { + t.Fatalf("did not expect validation error, got: %v", resp.Diagnostics) + } + } else { + hasExpectedError := false + + for _, diag := range resp.Diagnostics { + if diag.Summary() == "Attribute can not be set" { + hasExpectedError = true + } else { + t.Fatalf("expected validation error, got %q", diag.Summary()) + } + } + + if !hasExpectedError { + t.Fatalf("expected 'Attribute can not be set' error, got: %v", resp.Diagnostics) + } + } + }) + } +}