Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bundle/manifests/monitoring.rhobs_monitoringstacks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,12 @@ spec:
- conditions
type: object
type: object
x-kubernetes-validations:
- message: MonitoringStack name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
- message: MonitoringStack name must consist of lowercase alphanumeric characters
or '-', and must start and end with an alphanumeric character
rule: self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
served: true
storage: true
subresources:
Expand Down
6 changes: 6 additions & 0 deletions bundle/manifests/monitoring.rhobs_thanosqueriers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ spec:
It should always be reconstructable from the state of the cluster and/or outside world.
type: object
type: object
x-kubernetes-validations:
- message: ThanosQuerier name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
- message: ThanosQuerier name must consist of lowercase alphanumeric characters
or '-', and must start and end with an alphanumeric character
rule: self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
served: true
storage: true
subresources:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ spec:
type: string
type: object
type: object
x-kubernetes-validations:
- message: ObservabilityInstaller name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
- message: ObservabilityInstaller name must consist of lowercase alphanumeric
characters or '-', and must start and end with an alphanumeric character
rule: self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
served: true
storage: true
subresources:
Expand Down
6 changes: 6 additions & 0 deletions deploy/crds/common/monitoring.rhobs_monitoringstacks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,12 @@ spec:
- conditions
type: object
type: object
x-kubernetes-validations:
- message: MonitoringStack name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
- message: MonitoringStack name must consist of lowercase alphanumeric characters
or '-', and must start and end with an alphanumeric character
rule: self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
served: true
storage: true
subresources:
Expand Down
6 changes: 6 additions & 0 deletions deploy/crds/common/monitoring.rhobs_thanosqueriers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ spec:
It should always be reconstructable from the state of the cluster and/or outside world.
type: object
type: object
x-kubernetes-validations:
- message: ThanosQuerier name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
- message: ThanosQuerier name must consist of lowercase alphanumeric characters
or '-', and must start and end with an alphanumeric character
rule: self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
served: true
storage: true
subresources:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,12 @@ spec:
type: string
type: object
type: object
x-kubernetes-validations:
- message: ObservabilityInstaller name must be no more than 63 characters
rule: size(self.metadata.name) <= 63
- message: ObservabilityInstaller name must consist of lowercase alphanumeric
characters or '-', and must start and end with an alphanumeric character
rule: self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
served: true
storage: true
subresources:
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/monitoring/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
// +kubebuilder:resource
// +kubebuilder:subresource:status
// +kubebuilder:metadata:annotations="observability.openshift.io/api-support=GeneralAvailability"
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 63",message="MonitoringStack name must be no more than 63 characters"
// +kubebuilder:validation:XValidation:rule="self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')",message="MonitoringStack name must consist of lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character"
type MonitoringStack struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down Expand Up @@ -334,6 +336,8 @@ type NamespaceSelector struct {
// +kubebuilder:resource
// +kubebuilder:subresource:status
// +kubebuilder:metadata:annotations="observability.openshift.io/api-support=TechPreview"
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 63",message="ThanosQuerier name must be no more than 63 characters"
// +kubebuilder:validation:XValidation:rule="self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')",message="ThanosQuerier name must consist of lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character"
type ThanosQuerier struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
159 changes: 159 additions & 0 deletions pkg/apis/monitoring/v1alpha1/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package v1alpha1

import (
"fmt"
"os"
"regexp"
"strings"
"testing"

"github.com/google/cel-go/cel"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// extractValidationRules extracts the two XValidation rules from the comment block
// immediately before the named struct in types.go.
func extractValidationRules(structName string) (nameLengthRule, nameFormatRule string, err error) {
data, err := os.ReadFile("types.go")
if err != nil {
return "", "", fmt.Errorf("failed to read source file: %v", err)
}

content := string(data)
pattern := regexp.MustCompile(`(?s)// ` + structName + ` [^\n]+(?:\n//[^\n]*)*\ntype ` + structName + ` struct`)
blockMatch := pattern.FindString(content)
if blockMatch == "" {
return "", "", fmt.Errorf("%s struct block not found in source", structName)
}

rulePattern := regexp.MustCompile(`\+kubebuilder:validation:XValidation:rule="([^"]+)"`)
matches := rulePattern.FindAllStringSubmatch(blockMatch, -1)
if len(matches) < 2 {
return "", "", fmt.Errorf("expected at least 2 XValidation rules on %s, found %d", structName, len(matches))
}

return matches[0][1], matches[1][1], nil
}

func newCELEnvForMetadata() (*cel.Env, error) {
return cel.NewEnv(cel.Variable("self", cel.MapType(cel.StringType, cel.DynType)))
}

func makeMetaSelf(name string) map[string]interface{} {
return map[string]interface{}{
"metadata": map[string]interface{}{
"name": name,
},
}
}

func runNameValidationTests(t *testing.T, rule string, tests []struct {
name string
instName string
expectValid bool
}) {
t.Helper()
env, err := newCELEnvForMetadata()
require.NoError(t, err)

ast, issues := env.Compile(rule)
require.Empty(t, issues)

program, err := env.Program(ast)
require.NoError(t, err)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out, _, err := program.Eval(map[string]interface{}{
"self": makeMetaSelf(tt.instName),
})
require.NoError(t, err)

result := out.Value().(bool)
if tt.expectValid {
assert.True(t, result, "Expected name %q to be valid (length %d)", tt.instName, len(tt.instName))
} else {
assert.False(t, result, "Expected name %q to be invalid (length %d)", tt.instName, len(tt.instName))
}
})
}
}

func TestMonitoringStackNameLengthValidation(t *testing.T) {
lengthRule, _, err := extractValidationRules("MonitoringStack")
require.NoError(t, err, "failed to extract name length rule from source annotation")

runNameValidationTests(t, lengthRule, []struct {
name string
instName string
expectValid bool
}{
{"short valid name", "my-stack", true},
{"single character", "a", true},
{"exactly 63 characters", strings.Repeat("a", 63), true},
{"64 characters - too long", strings.Repeat("a", 64), false},
{"253 character name", strings.Repeat("a", 253), false},
})
}

func TestMonitoringStackNameFormatValidation(t *testing.T) {
_, formatRule, err := extractValidationRules("MonitoringStack")
require.NoError(t, err, "failed to extract name format rule from source annotation")

runNameValidationTests(t, formatRule, []struct {
name string
instName string
expectValid bool
}{
{"single lowercase letter", "a", true},
{"lowercase letters and dashes", "my-stack", true},
{"letters numbers and dashes", "stack-1", true},
{"starts with digit", "1stack", true},
{"ends with digit", "stack1", true},
{"uppercase letter - invalid", "MyStack", false},
{"starts with dash - invalid", "-stack", false},
{"ends with dash - invalid", "stack-", false},
{"underscore - invalid", "my_stack", false},
{"dot separator - invalid", "my.stack", false},
})
}

func TestThanosQuerierNameLengthValidation(t *testing.T) {
lengthRule, _, err := extractValidationRules("ThanosQuerier")
require.NoError(t, err, "failed to extract name length rule from source annotation")

runNameValidationTests(t, lengthRule, []struct {
name string
instName string
expectValid bool
}{
{"short valid name", "my-querier", true},
{"single character", "a", true},
{"exactly 63 characters", strings.Repeat("a", 63), true},
{"64 characters - too long", strings.Repeat("a", 64), false},
{"253 character name", strings.Repeat("a", 253), false},
})
}

func TestThanosQuerierNameFormatValidation(t *testing.T) {
_, formatRule, err := extractValidationRules("ThanosQuerier")
require.NoError(t, err, "failed to extract name format rule from source annotation")

runNameValidationTests(t, formatRule, []struct {
name string
instName string
expectValid bool
}{
{"single lowercase letter", "q", true},
{"lowercase letters and dashes", "my-querier", true},
{"letters and numbers", "querier1", true},
{"starts with digit", "1querier", true},
{"ends with digit", "querier1", true},
{"uppercase letter - invalid", "MyQuerier", false},
{"starts with dash - invalid", "-querier", false},
{"ends with dash - invalid", "querier-", false},
{"underscore - invalid", "my_querier", false},
{"dot separator - invalid", "my.querier", false},
})
}
2 changes: 2 additions & 0 deletions pkg/apis/observability/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
// +operator-sdk:csv:customresourcedefinitions:displayName="Observability Installer"
// +operator-sdk:csv:customresourcedefinitions:description="Provides end-to-end observability capabilities with minimal configuration. Simplifies deployment and management of observability components such as tracing."
// +kubebuilder:metadata:annotations="observability.openshift.io/api-support=TechPreview"
// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 63",message="ObservabilityInstaller name must be no more than 63 characters"
// +kubebuilder:validation:XValidation:rule="self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')",message="ObservabilityInstaller name must consist of lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character"
type ObservabilityInstaller struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
Loading
Loading