diff --git a/stackit/internal/services/observability/alertgroup/resource.go b/stackit/internal/services/observability/alertgroup/resource.go index c4199cd7d..db2fe31fd 100644 --- a/stackit/internal/services/observability/alertgroup/resource.go +++ b/stackit/internal/services/observability/alertgroup/resource.go @@ -261,6 +261,7 @@ func (a *alertGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, mapvalidator.KeysAre(stringvalidator.LengthAtMost(200)), mapvalidator.ValueStringsAre(stringvalidator.LengthAtMost(200)), mapvalidator.SizeAtMost(5), + mapvalidator.ValueStringsAre(validate.NoLeadingOrTrailingWhitespace()), }, }, "record": schema.StringAttribute{ diff --git a/stackit/internal/validate/validate.go b/stackit/internal/validate/validate.go index 502187733..358fc6d3f 100644 --- a/stackit/internal/validate/validate.go +++ b/stackit/internal/validate/validate.go @@ -9,6 +9,7 @@ import ( "strings" "time" _ "time/tzdata" + "unicode" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" @@ -394,3 +395,29 @@ func IsLowercased() *Validator { }, } } + +// NoLeadingOrTrailingWhitespace returns a Validator that checks if the input string has leading or trailing whitespace. +// Examples: +// - "example": valid +// - "": valid, empty value +// - " example": invalid, leading whitespace +// - "example ": invalid, trailing whitespace +func NoLeadingOrTrailingWhitespace() *Validator { + description := "Value must not have leading or trailing whitespace, as defined by gos unicode.IsSpace(rune)." + return &Validator{ + description: description, + validate: func(_ context.Context, request validator.StringRequest, response *validator.StringResponse) { + val := request.ConfigValue.ValueString() + if val == "" { + return + } + if unicode.IsSpace(rune(val[0])) || unicode.IsSpace(rune(val[len(val)-1])) { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + description, + val, + )) + } + }, + } +} diff --git a/stackit/internal/validate/validate_test.go b/stackit/internal/validate/validate_test.go index c08a70d14..60dd83b45 100644 --- a/stackit/internal/validate/validate_test.go +++ b/stackit/internal/validate/validate_test.go @@ -1008,3 +1008,60 @@ func TestIsLowercased(t *testing.T) { }) } } + +func TestNoLeadingOrtTrailingWhitespace(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "empty", + input: "", + }, + { + name: "valid", + input: "valid", + }, + { + name: "single char, valid", + input: "a", + }, + { + name: "leading whitespace", + input: " leading", + wantErr: true, + }, + { + name: "trailing whitespace", + input: "trailing ", + wantErr: true, + }, + { + name: "leading and trailing whitespace", + input: " leading and trailing ", + wantErr: true, + }, + { + name: "single space", + input: " ", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := validator.StringResponse{} + NoLeadingOrTrailingWhitespace().ValidateString(context.Background(), validator.StringRequest{ + ConfigValue: types.StringValue(tt.input), + }, &r) + if tt.wantErr && !r.Diagnostics.HasError() { + t.Fatalf("Expected validation to fail for input: %q", tt.input) + } + if !tt.wantErr && r.Diagnostics.HasError() { + t.Fatalf("Expected validation to succeed for input: %q, but got errors: %v", tt.input, r.Diagnostics.Errors()) + } + }) + } +}