From 00e979f8ed85b4a72a2c55c3b5c630ba0c5d010c Mon Sep 17 00:00:00 2001 From: Adam Bernot Date: Fri, 10 Apr 2026 17:21:18 +0000 Subject: [PATCH 1/3] feat: implement CEL validations for OperatorConfig Adds kubebuilder CEL validations to `OperatorConfig` fields based on the migration away from webhook validation. - Validates prometheus label keys in `externalLabels` - Validates `queryProjectID` constraints (length and regex pattern) - Adds `isURL` checks for `generatorUrl`, `exports.url`, `externalURL` - Implements RFC 1123 label constraints for AlertmanagerEndpoints namespace and name - Validates TLSConfig KeySecret name - Enforces HTTP/HTTPS scheme for AlertmanagerEndpoints Signed-off-by: Adam Bernot --- ...toring.googleapis.com_operatorconfigs.yaml | 46 ++ e2e/crd_validation_test.go | 445 +++++++++++++++++- manifests/fs.go | 2 + manifests/setup.yaml | 38 ++ .../apis/monitoring/v1/operator_types.go | 19 + 5 files changed, 534 insertions(+), 16 deletions(-) diff --git a/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml b/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml index dc435aa295..b038db523f 100644 --- a/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml +++ b/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml @@ -78,6 +78,9 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') + && size(self.name) <= 253 externalLabels: additionalProperties: type: string @@ -86,6 +89,9 @@ spec: data before being written to Google Cloud Monitoring or any other additional exports specified in the OperatorConfig. The precedence behavior matches that of Prometheus. type: object + x-kubernetes-validations: + - message: Invalid label key + rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) filter: description: Filter limits which metric data is sent to Cloud Monitoring (it doesn't apply to additional exports). @@ -157,9 +163,12 @@ spec: description: The URL of the endpoint that supports Prometheus Remote Write to export samples to. type: string + x-kubernetes-validations: + - rule: self == '' || isURL(self) required: - url type: object + maxItems: 10 type: array features: description: Features holds configuration for optional managed-collection @@ -227,6 +236,9 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') + && size(self.name) <= 253 externalURL: description: |- ExternalURL is the URL under which Alertmanager is externally reachable (for example, if @@ -237,6 +249,8 @@ spec: If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page. type: string + x-kubernetes-validations: + - rule: self == '' || isURL(self) type: object metadata: type: object @@ -288,6 +302,9 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') + && size(self.name) <= 253 type: description: |- Set the authentication type. Defaults to Bearer, Basic will cause an @@ -296,9 +313,13 @@ spec: type: object name: description: Name of Endpoints object in Namespace. + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string namespace: description: Namespace of Endpoints object. + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string pathPrefix: description: Prefix for the HTTP path alerts are pushed @@ -312,6 +333,9 @@ spec: x-kubernetes-int-or-string: true scheme: description: Scheme to use when firing alerts. + enum: + - http + - https type: string timeout: description: Timeout is a per-target Alertmanager timeout @@ -373,6 +397,9 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') + && size(self.name) <= 253 type: object cert: description: Struct containing the client cert file @@ -427,6 +454,9 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') + && size(self.name) <= 253 type: object insecureSkipVerify: description: Disable target certificate validation. @@ -456,6 +486,9 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') + && size(self.name) <= 253 maxVersion: description: |- Maximum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3). @@ -477,6 +510,7 @@ spec: - namespace - port type: object + maxItems: 3 type: array type: object credentials: @@ -508,6 +542,9 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') + && size(self.name) <= 253 externalLabels: additionalProperties: type: string @@ -516,17 +553,26 @@ spec: results and alerts produced by rules. The precedence behavior matches that of Prometheus. type: object + x-kubernetes-validations: + - message: Invalid label key + rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) generatorUrl: description: |- The base URL used for the generator URL in the alert notification payload. Should point to an instance of a query frontend that gives access to queryProjectID. type: string + x-kubernetes-validations: + - rule: self == '' || isURL(self) queryProjectID: description: |- QueryProjectID is the GCP project ID to evaluate rules against. If left blank, the rule-evaluator will try attempt to infer the Project ID from the environment. type: string + x-kubernetes-validations: + - message: Invalid GCP project ID + rule: self == '' || (self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') && + size(self) >= 6 && size(self) <= 30) type: object scaling: description: Scaling contains configuration options for scaling GMP. diff --git a/e2e/crd_validation_test.go b/e2e/crd_validation_test.go index d4704e6033..acffface60 100644 --- a/e2e/crd_validation_test.go +++ b/e2e/crd_validation_test.go @@ -15,18 +15,24 @@ package e2e import ( + "context" _ "embed" "fmt" "os/exec" "path/filepath" + "strings" "testing" + "time" monitoringv1 "github.com/GoogleCloudPlatform/prometheus-engine/pkg/operator/apis/monitoring/v1" "github.com/google/go-cmp/cmp" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -443,6 +449,11 @@ func TestCRDValidation(t *testing.T) { switch { case err == nil && !tc.wantErr: // OK + _ = c.Delete(t.Context(), tc.obj) + _ = wait.PollUntilContextTimeout(t.Context(), 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) { + err := c.Get(ctx, client.ObjectKeyFromObject(tc.obj), tc.obj.DeepCopyObject().(client.Object)) + return apierrors.IsNotFound(err), nil + }) case err != nil && !tc.wantErr: t.Errorf("Unexpected error: %v", err) case err == nil && tc.wantErr: @@ -601,7 +612,7 @@ func TestCRDValidation(t *testing.T) { Namespace: "gmp-public", }, Collection: monitoringv1.CollectionSpec{ - Credentials: &v1.SecretKeySelector{}, + Credentials: &corev1.SecretKeySelector{}, }, }, wantErr: true, @@ -625,7 +636,7 @@ func TestCRDValidation(t *testing.T) { Namespace: "gmp-public", }, Rules: monitoringv1.RuleEvaluatorSpec{ - Credentials: &v1.SecretKeySelector{}, + Credentials: &corev1.SecretKeySelector{}, }, }, wantErr: true, @@ -637,7 +648,7 @@ func TestCRDValidation(t *testing.T) { Namespace: "gmp-public", }, ManagedAlertmanager: &monitoringv1.ManagedAlertmanagerSpec{ - ConfigSecret: &v1.SecretKeySelector{}, + ConfigSecret: &corev1.SecretKeySelector{}, }, }, wantErr: true, @@ -654,7 +665,7 @@ func TestCRDValidation(t *testing.T) { Name: "bar", TLS: &monitoringv1.TLSConfig{}, Authorization: &monitoringv1.Authorization{ - Credentials: &v1.SecretKeySelector{}, + Credentials: &corev1.SecretKeySelector{}, }, }}, }, @@ -673,7 +684,7 @@ func TestCRDValidation(t *testing.T) { Alertmanagers: []monitoringv1.AlertmanagerEndpoints{{ Name: "bar", TLS: &monitoringv1.TLSConfig{ - KeySecret: &v1.SecretKeySelector{}, + KeySecret: &corev1.SecretKeySelector{}, }, }}, }, @@ -693,13 +704,13 @@ func TestCRDValidation(t *testing.T) { Name: "bar", TLS: &monitoringv1.TLSConfig{ CA: &monitoringv1.SecretOrConfigMap{ - Secret: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ + Secret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ Name: "baz", }, }, - ConfigMap: &v1.ConfigMapKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ + ConfigMap: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ Name: "qux", }, }, @@ -723,7 +734,7 @@ func TestCRDValidation(t *testing.T) { Name: "bar", TLS: &monitoringv1.TLSConfig{ CA: &monitoringv1.SecretOrConfigMap{ - Secret: &v1.SecretKeySelector{}, + Secret: &corev1.SecretKeySelector{}, }, }, }}, @@ -744,13 +755,13 @@ func TestCRDValidation(t *testing.T) { Name: "bar", TLS: &monitoringv1.TLSConfig{ Cert: &monitoringv1.SecretOrConfigMap{ - Secret: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ + Secret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ Name: "baz", }, }, - ConfigMap: &v1.ConfigMapKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ + ConfigMap: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ Name: "qux", }, }, @@ -774,7 +785,7 @@ func TestCRDValidation(t *testing.T) { Name: "bar", TLS: &monitoringv1.TLSConfig{ Cert: &monitoringv1.SecretOrConfigMap{ - Secret: &v1.SecretKeySelector{}, + Secret: &corev1.SecretKeySelector{}, }, }, }}, @@ -791,6 +802,408 @@ func TestCRDValidation(t *testing.T) { }, }, }, + "valid credentials secret name (RFC 1123 subdomain)": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + Collection: monitoringv1.CollectionSpec{ + Credentials: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my-secret.v1", + }, + Key: "key.json", + }, + }, + }, + }, + "invalid credentials secret name": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + Collection: monitoringv1.CollectionSpec{ + Credentials: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my_secret", + }, + Key: "key.json", + }, + }, + }, + wantErr: true, + }, + "credentials secret name too long": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + Collection: monitoringv1.CollectionSpec{ + Credentials: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: strings.Repeat("a", 254), + }, + Key: "key.json", + }, + }, + }, + wantErr: true, + }, + "queryProjectID empty string": { + obj: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]any{ + "name": "config", + "namespace": "gmp-public", + }, + "rules": map[string]any{ + "queryProjectID": "", + }, + }, + }, + wantErr: false, + }, + "queryProjectID valid": { + obj: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]any{ + "name": "config", + "namespace": "gmp-public", + }, + "rules": map[string]any{ + "queryProjectID": "my-valid-project", + }, + }, + }, + wantErr: false, + }, + "queryProjectID invalid pattern": { + obj: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]any{ + "name": "config-query-project-id-invalid-pattern", + "namespace": "gmp-public", + }, + "rules": map[string]any{ + "queryProjectID": "bad!", + }, + }, + }, + wantErr: true, + }, + "queryProjectID too short": { + obj: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]any{ + "name": "config-query-project-id-too-short", + "namespace": "gmp-public", + }, + "rules": map[string]any{ + "queryProjectID": "abc", + }, + }, + }, + wantErr: true, + }, + "valid externalLabels": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + Collection: monitoringv1.CollectionSpec{ + ExternalLabels: map[string]string{ + "env": "production", + }, + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + ExternalLabels: map[string]string{ + "region": "us-central1", + }, + }, + }, + wantErr: false, + }, + "collection externalLabels invalid key": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-invalid-collection-labels", + Namespace: "gmp-public", + }, + Collection: monitoringv1.CollectionSpec{ + ExternalLabels: map[string]string{ + "0invalid-key": "value", + }, + }, + }, + wantErr: true, + }, + "rules externalLabels invalid key": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-invalid-rules-labels", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + ExternalLabels: map[string]string{ + "invalid.key": "value", + }, + }, + }, + wantErr: true, + }, + "queryProjectID too long": { + obj: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]any{ + "name": "config-query-project-id-too-long", + "namespace": "gmp-public", + }, + "rules": map[string]any{ + "queryProjectID": "a-project-id-that-is-way-too-long-to-be-valid", + }, + }, + }, + wantErr: true, + }, + "valid generator URL": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + GeneratorURL: "https://example.com/graph", + }, + }, + wantErr: false, + }, + "valid exports URL": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + Exports: []monitoringv1.ExportSpec{ + { + URL: "https://remote-write.example.com/api/v1/write", + }, + }, + }, + wantErr: false, + }, + "bad exports URL": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-bad-exports-url", + Namespace: "gmp-public", + }, + Exports: []monitoringv1.ExportSpec{ + { + URL: "~:://example.com", + }, + }, + }, + wantErr: true, + }, + "valid externalURL": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + ManagedAlertmanager: &monitoringv1.ManagedAlertmanagerSpec{ + ConfigSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "alertmanager"}, + Key: "alertmanager.yaml", + }, + ExternalURL: "https://alertmanager.example.com", + }, + }, + wantErr: false, + }, + "bad externalURL": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-bad-external-url", + Namespace: "gmp-public", + }, + ManagedAlertmanager: &monitoringv1.ManagedAlertmanagerSpec{ + ConfigSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "alertmanager"}, + Key: "alertmanager.yaml", + }, + ExternalURL: "~:://example.com", + }, + }, + wantErr: true, + }, + "valid AlertmanagerEndpoints": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + Alerting: monitoringv1.AlertingSpec{ + Alertmanagers: []monitoringv1.AlertmanagerEndpoints{ + { + Namespace: "monitoring", + Name: "alertmanager-operated", + Port: intstr.FromString("web"), + Scheme: "https", + }, + }, + }, + }, + }, + wantErr: false, + }, + "AlertmanagerEndpoints invalid namespace": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-alertmanager-invalid-ns", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + Alerting: monitoringv1.AlertingSpec{ + Alertmanagers: []monitoringv1.AlertmanagerEndpoints{ + { + Namespace: "invalid.ns!", + Name: "alertmanager-operated", + Port: intstr.FromString("web"), + Scheme: "http", + }, + }, + }, + }, + }, + wantErr: true, + }, + "AlertmanagerEndpoints invalid name": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-alertmanager-invalid-name", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + Alerting: monitoringv1.AlertingSpec{ + Alertmanagers: []monitoringv1.AlertmanagerEndpoints{ + { + Namespace: "monitoring", + Name: "invalid_name", + Port: intstr.FromString("web"), + Scheme: "http", + }, + }, + }, + }, + }, + wantErr: true, + }, + "AlertmanagerEndpoints invalid scheme": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-alertmanager-invalid-scheme", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + Alerting: monitoringv1.AlertingSpec{ + Alertmanagers: []monitoringv1.AlertmanagerEndpoints{ + { + Namespace: "monitoring", + Name: "alertmanager-operated", + Port: intstr.FromString("web"), + Scheme: "grpc", + }, + }, + }, + }, + }, + wantErr: true, + }, + "too many exports": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-too-many-exports", + Namespace: "gmp-public", + }, + Exports: func() []monitoringv1.ExportSpec { + var exports []monitoringv1.ExportSpec + for i := range 11 { + exports = append(exports, monitoringv1.ExportSpec{ + URL: fmt.Sprintf("https://example.com/write-%d", i), + }) + } + return exports + }(), + }, + wantErr: true, + }, + "too many alertmanagers": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-too-many-alertmanagers", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + Alerting: monitoringv1.AlertingSpec{ + Alertmanagers: func() []monitoringv1.AlertmanagerEndpoints { + var ams []monitoringv1.AlertmanagerEndpoints + for i := range 4 { + ams = append(ams, monitoringv1.AlertmanagerEndpoints{ + Namespace: "monitoring", + Name: fmt.Sprintf("am-%d", i), + Port: intstr.FromString("web"), + Scheme: "http", + }) + } + return ams + }(), + }, + }, + }, + wantErr: true, + }, + "invalid TLS key secret name": { + obj: &monitoringv1.OperatorConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config-invalid-tls-key-secret", + Namespace: "gmp-public", + }, + Rules: monitoringv1.RuleEvaluatorSpec{ + Alerting: monitoringv1.AlertingSpec{ + Alertmanagers: []monitoringv1.AlertmanagerEndpoints{{ + Namespace: "monitoring", + Name: "alertmanager-operated", + Port: intstr.FromString("web"), + TLS: &monitoringv1.TLSConfig{ + KeySecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "my_invalid_secret", + }, + Key: "tls.key", + }, + }, + }}, + }, + }, + }, + wantErr: true, + }, } run(t, tests) }) diff --git a/manifests/fs.go b/manifests/fs.go index 4146a0c33d..a8fa087106 100644 --- a/manifests/fs.go +++ b/manifests/fs.go @@ -23,5 +23,7 @@ import _ "embed" //go:embed operator.yaml var OperatorManifest []byte +// CRDManifest contains the OperatorConfig and GMP CRDs. +// //go:embed setup.yaml var CRDManifest []byte diff --git a/manifests/setup.yaml b/manifests/setup.yaml index ed34540c5c..7d08070d0a 100644 --- a/manifests/setup.yaml +++ b/manifests/setup.yaml @@ -1913,6 +1913,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 externalLabels: additionalProperties: type: string @@ -1921,6 +1923,9 @@ spec: data before being written to Google Cloud Monitoring or any other additional exports specified in the OperatorConfig. The precedence behavior matches that of Prometheus. type: object + x-kubernetes-validations: + - message: Invalid label key + rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) filter: description: Filter limits which metric data is sent to Cloud Monitoring (it doesn't apply to additional exports). properties: @@ -1990,9 +1995,12 @@ spec: url: description: The URL of the endpoint that supports Prometheus Remote Write to export samples to. type: string + x-kubernetes-validations: + - rule: self == '' || isURL(self) required: - url type: object + maxItems: 10 type: array features: description: Features holds configuration for optional managed-collection features. @@ -2057,6 +2065,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 externalURL: description: |- ExternalURL is the URL under which Alertmanager is externally reachable (for example, if @@ -2067,6 +2077,8 @@ spec: If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page. type: string + x-kubernetes-validations: + - rule: self == '' || isURL(self) type: object metadata: type: object @@ -2113,6 +2125,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 type: description: |- Set the authentication type. Defaults to Bearer, Basic will cause an @@ -2121,9 +2135,13 @@ spec: type: object name: description: Name of Endpoints object in Namespace. + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string namespace: description: Namespace of Endpoints object. + maxLength: 63 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string pathPrefix: description: Prefix for the HTTP path alerts are pushed to. @@ -2136,6 +2154,9 @@ spec: x-kubernetes-int-or-string: true scheme: description: Scheme to use when firing alerts. + enum: + - http + - https type: string timeout: description: Timeout is a per-target Alertmanager timeout when pushing alerts. @@ -2190,6 +2211,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 type: object cert: description: Struct containing the client cert file for the targets. @@ -2238,6 +2261,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 type: object insecureSkipVerify: description: Disable target certificate validation. @@ -2264,6 +2289,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 maxVersion: description: |- Maximum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3). @@ -2285,6 +2312,7 @@ spec: - namespace - port type: object + maxItems: 3 type: array type: object credentials: @@ -2315,6 +2343,8 @@ spec: - key type: object x-kubernetes-map-type: atomic + x-kubernetes-validations: + - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 externalLabels: additionalProperties: type: string @@ -2323,17 +2353,25 @@ spec: results and alerts produced by rules. The precedence behavior matches that of Prometheus. type: object + x-kubernetes-validations: + - message: Invalid label key + rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) generatorUrl: description: |- The base URL used for the generator URL in the alert notification payload. Should point to an instance of a query frontend that gives access to queryProjectID. type: string + x-kubernetes-validations: + - rule: self == '' || isURL(self) queryProjectID: description: |- QueryProjectID is the GCP project ID to evaluate rules against. If left blank, the rule-evaluator will try attempt to infer the Project ID from the environment. type: string + x-kubernetes-validations: + - message: Invalid GCP project ID + rule: self == '' || (self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') && size(self) >= 6 && size(self) <= 30) type: object scaling: description: Scaling contains configuration options for scaling GMP. diff --git a/pkg/operator/apis/monitoring/v1/operator_types.go b/pkg/operator/apis/monitoring/v1/operator_types.go index a3fb792188..d51145535e 100644 --- a/pkg/operator/apis/monitoring/v1/operator_types.go +++ b/pkg/operator/apis/monitoring/v1/operator_types.go @@ -40,6 +40,7 @@ type OperatorConfig struct { // Exports is an EXPERIMENTAL feature that specifies additional, optional endpoints to export to, // on top of Google Cloud Monitoring collection. // Note: To disable integrated export to Google Cloud Monitoring specify a non-matching filter in the "collection.filter" field. + // +kubebuilder:validation:MaxItems=10 Exports []ExportSpec `json:"exports,omitempty"` // ManagedAlertmanager holds information for configuring the managed instance of Alertmanager. // +kubebuilder:default={configSecret: {name: alertmanager, key: alertmanager.yaml}} @@ -146,13 +147,16 @@ type RuleEvaluatorSpec struct { // ExternalLabels specifies external labels that are attached to any rule // results and alerts produced by rules. The precedence behavior matches that // of Prometheus. + // +kubebuilder:validation:XValidation:rule="self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))",message="Invalid label key" ExternalLabels map[string]string `json:"externalLabels,omitempty"` // QueryProjectID is the GCP project ID to evaluate rules against. // If left blank, the rule-evaluator will try attempt to infer the Project ID // from the environment. + // +kubebuilder:validation:XValidation:rule="self == '' || (self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') && size(self) >= 6 && size(self) <= 30)",message="Invalid GCP project ID" QueryProjectID string `json:"queryProjectID,omitempty"` // The base URL used for the generator URL in the alert notification payload. // Should point to an instance of a query frontend that gives access to queryProjectID. + // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)" GeneratorURL string `json:"generatorUrl,omitempty"` // Alerting contains how the rule-evaluator configures alerting. Alerting AlertingSpec `json:"alerting,omitempty"` @@ -162,6 +166,7 @@ type RuleEvaluatorSpec struct { // to which rule results are written. // Within GKE, this can typically be left empty if the compute default // service account has the required permissions. + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" Credentials *corev1.SecretKeySelector `json:"credentials,omitempty"` } @@ -170,6 +175,7 @@ type CollectionSpec struct { // ExternalLabels specifies external labels that are attached to all scraped // data before being written to Google Cloud Monitoring or any other additional exports // specified in the OperatorConfig. The precedence behavior matches that of Prometheus. + // +kubebuilder:validation:XValidation:rule="self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))",message="Invalid label key" ExternalLabels map[string]string `json:"externalLabels,omitempty"` // Filter limits which metric data is sent to Cloud Monitoring (it doesn't apply to additional exports). Filter ExportFilters `json:"filter,omitempty"` @@ -178,6 +184,7 @@ type CollectionSpec struct { // data is written. // Within GKE, this can typically be left empty if the compute default // service account has the required permissions. + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" Credentials *corev1.SecretKeySelector `json:"credentials,omitempty"` // Configuration to scrape the metric endpoints of the Kubelets. KubeletScraping *KubeletScraping `json:"kubeletScraping,omitempty"` @@ -187,6 +194,7 @@ type CollectionSpec struct { type ExportSpec struct { // The URL of the endpoint that supports Prometheus Remote Write to export samples to. + // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)" URL string `json:"url"` } @@ -272,6 +280,7 @@ type ExportFilters struct { // AlertingSpec defines alerting configuration. type AlertingSpec struct { // Alertmanagers contains endpoint configuration for designated Alertmanagers. + // +kubebuilder:validation:MaxItems=3 Alertmanagers []AlertmanagerEndpoints `json:"alertmanagers,omitempty"` } @@ -280,6 +289,7 @@ type AlertingSpec struct { type ManagedAlertmanagerSpec struct { // ConfigSecret refers to the name of a single-key Secret in the public namespace that // holds the managed Alertmanager config file. + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" ConfigSecret *corev1.SecretKeySelector `json:"configSecret,omitempty"` // ExternalURL is the URL under which Alertmanager is externally reachable (for example, if // Alertmanager is served via a reverse proxy). Used for generating relative and absolute @@ -288,6 +298,7 @@ type ManagedAlertmanagerSpec struct { // be derived automatically. // // If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page. + // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)" ExternalURL string `json:"externalURL,omitempty"` } @@ -295,12 +306,17 @@ type ManagedAlertmanagerSpec struct { // containing alertmanager IPs to fire alerts against. type AlertmanagerEndpoints struct { // Namespace of Endpoints object. + // +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + // +kubebuilder:validation:MaxLength=63 Namespace string `json:"namespace"` // Name of Endpoints object in Namespace. + // +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + // +kubebuilder:validation:MaxLength=63 Name string `json:"name"` // Port the Alertmanager API is exposed on. Port intstr.IntOrString `json:"port"` // Scheme to use when firing alerts. + // +kubebuilder:validation:Enum=http;https Scheme string `json:"scheme,omitempty"` // Prefix for the HTTP path alerts are pushed to. PathPrefix string `json:"pathPrefix,omitempty"` @@ -322,6 +338,7 @@ type Authorization struct { // error Type string `json:"type,omitempty"` // The secret's key that contains the credentials of the request + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" Credentials *corev1.SecretKeySelector `json:"credentials,omitempty"` } @@ -332,6 +349,7 @@ type TLSConfig struct { // Struct containing the client cert file for the targets. Cert *SecretOrConfigMap `json:"cert,omitempty"` // Secret containing the client key file for the targets. + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" KeySecret *corev1.SecretKeySelector `json:"keySecret,omitempty"` // Used to verify the hostname for the targets. ServerName string `json:"serverName,omitempty"` @@ -351,6 +369,7 @@ type TLSConfig struct { // Taking inspiration from prometheus-operator: https://github.com/prometheus-operator/prometheus-operator/blob/2c81b0cf6a5673e08057499a08ddce396b19dda4/Documentation/api.md#secretorconfigmap type SecretOrConfigMap struct { // Secret containing data to use for the targets. + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" Secret *corev1.SecretKeySelector `json:"secret,omitempty"` // ConfigMap containing data to use for the targets. ConfigMap *corev1.ConfigMapKeySelector `json:"configMap,omitempty"` From e5d8bdca6ac820c999d1b5271952a67ba969cc8c Mon Sep 17 00:00:00 2001 From: Adam Bernot Date: Thu, 25 Jun 2026 15:12:24 -0700 Subject: [PATCH 2/3] test: add fuzzing of operatorconfig validation --- ...toring.googleapis.com_operatorconfigs.yaml | 73 ++-- go.mod | 10 +- go.sum | 36 ++ manifests/setup.yaml | 65 ++-- .../v1/operator_config_fuzz_test.go | 312 ++++++++++++++++++ .../apis/monitoring/v1/operator_types.go | 35 +- .../fuzz/FuzzOperatorConfig/0532d422a788de2f | 2 + .../fuzz/FuzzOperatorConfig/243948b7fbb800b6 | 2 + .../fuzz/FuzzOperatorConfig/2586d20a23bef658 | 2 + .../fuzz/FuzzOperatorConfig/3ec1a05a2fbed299 | 2 + .../fuzz/FuzzOperatorConfig/3f20a2b3c8d47659 | 2 + .../fuzz/FuzzOperatorConfig/4a50f58542ae9d7b | 2 + .../fuzz/FuzzOperatorConfig/525880831c2ae64e | 2 + .../fuzz/FuzzOperatorConfig/5398a4ce76e5f19c | 2 + .../fuzz/FuzzOperatorConfig/5b295d28608a74f3 | 2 + .../fuzz/FuzzOperatorConfig/7f42ed90ec4df77f | 2 + .../fuzz/FuzzOperatorConfig/8bb902370535cab5 | 2 + .../fuzz/FuzzOperatorConfig/a41cfcf5df312c4a | 2 + .../fuzz/FuzzOperatorConfig/d3dff42ddb7b3b6e | 2 + .../fuzz/FuzzOperatorConfig/e567f7d4ad6ebe42 | 2 + .../monitoring/v1/zz_generated.deepcopy.go | 6 +- 21 files changed, 465 insertions(+), 100 deletions(-) create mode 100644 pkg/operator/apis/monitoring/v1/operator_config_fuzz_test.go create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/0532d422a788de2f create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/243948b7fbb800b6 create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/2586d20a23bef658 create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3ec1a05a2fbed299 create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3f20a2b3c8d47659 create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/4a50f58542ae9d7b create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/525880831c2ae64e create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5398a4ce76e5f19c create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5b295d28608a74f3 create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/7f42ed90ec4df77f create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/8bb902370535cab5 create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/a41cfcf5df312c4a create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/d3dff42ddb7b3b6e create mode 100644 pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/e567f7d4ad6ebe42 diff --git a/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml b/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml index b038db523f..5d2572ae7d 100644 --- a/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml +++ b/charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml @@ -46,9 +46,6 @@ spec: compression: description: Compression enables compression of metrics collection data - enum: - - none - - gzip type: string credentials: description: |- @@ -79,8 +76,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') - && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' externalLabels: additionalProperties: type: string @@ -89,9 +86,6 @@ spec: data before being written to Google Cloud Monitoring or any other additional exports specified in the OperatorConfig. The precedence behavior matches that of Prometheus. type: object - x-kubernetes-validations: - - message: Invalid label key - rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) filter: description: Filter limits which metric data is sent to Cloud Monitoring (it doesn't apply to additional exports). @@ -142,6 +136,7 @@ spec: properties: interval: description: The interval at which the metric endpoints are scraped. + format: duration type: string tlsInsecureSkipVerify: description: |- @@ -162,13 +157,14 @@ spec: url: description: The URL of the endpoint that supports Prometheus Remote Write to export samples to. + format: uri type: string x-kubernetes-validations: - - rule: self == '' || isURL(self) + - message: url must be a valid URL + rule: self == '' || isURL(self) required: - url type: object - maxItems: 10 type: array features: description: Features holds configuration for optional managed-collection @@ -182,9 +178,6 @@ spec: Compression enables compression of the config data propagated by the operator to collectors and the rule-evaluator. It is recommended to use the gzip option when using a large number of ClusterPodMonitoring, PodMonitoring, GlobalRules, ClusterRules, and/or Rules. - enum: - - none - - gzip type: string type: object targetStatus: @@ -237,8 +230,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') - && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' externalURL: description: |- ExternalURL is the URL under which Alertmanager is externally reachable (for example, if @@ -248,9 +241,11 @@ spec: be derived automatically. If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page. + format: uri type: string x-kubernetes-validations: - - rule: self == '' || isURL(self) + - message: externalURL must be a valid URL + rule: self == '' || isURL(self) type: object metadata: type: object @@ -303,8 +298,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') - && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' type: description: |- Set the authentication type. Defaults to Bearer, Basic will cause an @@ -313,13 +308,9 @@ spec: type: object name: description: Name of Endpoints object in Namespace. - maxLength: 63 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string namespace: description: Namespace of Endpoints object. - maxLength: 63 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string pathPrefix: description: Prefix for the HTTP path alerts are pushed @@ -333,13 +324,11 @@ spec: x-kubernetes-int-or-string: true scheme: description: Scheme to use when firing alerts. - enum: - - http - - https type: string timeout: description: Timeout is a per-target Alertmanager timeout when pushing alerts. + format: duration type: string tls: description: TLS Config to use for alertmanager connection. @@ -398,9 +387,12 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') - && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' type: object + x-kubernetes-validations: + - message: SecretOrConfigMap fields are mutually exclusive + rule: '!(has(self.secret) && has(self.configMap))' cert: description: Struct containing the client cert file for the targets. @@ -455,9 +447,12 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') - && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' type: object + x-kubernetes-validations: + - message: SecretOrConfigMap fields are mutually exclusive + rule: '!(has(self.secret) && has(self.configMap))' insecureSkipVerify: description: Disable target certificate validation. type: boolean @@ -487,8 +482,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') - && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' maxVersion: description: |- Maximum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3). @@ -510,7 +505,6 @@ spec: - namespace - port type: object - maxItems: 3 type: array type: object credentials: @@ -543,8 +537,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') - && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' externalLabels: additionalProperties: type: string @@ -553,26 +547,21 @@ spec: results and alerts produced by rules. The precedence behavior matches that of Prometheus. type: object - x-kubernetes-validations: - - message: Invalid label key - rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) generatorUrl: description: |- The base URL used for the generator URL in the alert notification payload. Should point to an instance of a query frontend that gives access to queryProjectID. + format: uri type: string x-kubernetes-validations: - - rule: self == '' || isURL(self) + - message: generatorUrl must be a valid URL + rule: self == '' || isURL(self) queryProjectID: description: |- QueryProjectID is the GCP project ID to evaluate rules against. If left blank, the rule-evaluator will try attempt to infer the Project ID from the environment. type: string - x-kubernetes-validations: - - message: Invalid GCP project ID - rule: self == '' || (self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') && - size(self) >= 6 && size(self) <= 30) type: object scaling: description: Scaling contains configuration options for scaling GMP. diff --git a/go.mod b/go.mod index e493a1e017..abe4d249aa 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,10 @@ require ( sigs.k8s.io/controller-runtime v0.18.7 ) -require github.com/efficientgo/e2e v0.14.1-0.20230710114240-c316eb95ae5b +require ( + github.com/efficientgo/e2e v0.14.1-0.20230710114240-c316eb95ae5b + k8s.io/apiserver v0.30.14 +) require ( cloud.google.com/go/auth v0.16.5 // indirect @@ -55,6 +58,7 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.55.8 // indirect github.com/aws/aws-sdk-go-v2 v1.38.1 // indirect @@ -72,6 +76,7 @@ require ( github.com/aws/smithy-go v1.22.5 // indirect github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -105,6 +110,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect + github.com/google/cel-go v0.17.8 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect @@ -144,6 +150,7 @@ require ( github.com/prometheus/procfs v0.17.0 // indirect github.com/prometheus/sigv4 v0.2.1 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.mongodb.org/mongo-driver v1.17.7 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -171,6 +178,7 @@ require ( google.golang.org/genproto v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/component-base v0.30.14 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index fd5ab0a710..ecd4e66867 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HR github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -75,6 +77,10 @@ github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -84,6 +90,10 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -183,6 +193,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/cel-go v0.17.8 h1:j9m730pMZt1Fc4oKhCLUHfjj6527LuhYcYw0Rl8gqto= +github.com/google/cel-go v0.17.8/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -221,6 +233,9 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hashicorp/consul/api v1.29.1 h1:UEwOjYJrd3lG1x5w7HxDRMGiAUPrb3f103EoeKuuEcc= github.com/hashicorp/consul/api v1.29.1/go.mod h1:lumfRkY/coLuqMICkI7Fh3ylMG31mQSRZyef2c5YvJI= github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A= @@ -375,6 +390,8 @@ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27 h1:yGAraK1uUjlhSXgNMIy8o/J4LF github.com/scaleway/scaleway-sdk-go v1.0.0-beta.27/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -382,6 +399,7 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -400,6 +418,12 @@ github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtX github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -416,6 +440,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/X go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= @@ -424,6 +452,8 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/slim/otlp v1.7.1 h1:lZ11gEokjIWYM3JWOUrIILr2wcf6RX+rq5SPObV9oyc= go.opentelemetry.io/proto/slim/otlp v1.7.1/go.mod h1:uZ6LJWa49eNM/EXnnvJGTTu8miokU8RQdnO980LJ57g= go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.0.1 h1:Tr/eXq6N7ZFjN+THBF/BtGLUz8dciA7cuzGRsCEkZ88= @@ -606,16 +636,22 @@ k8s.io/apiextensions-apiserver v0.30.14 h1:7hi4CMq9EVT4vjdFpDZ/E1yOqhfU5IDHl2M8b k8s.io/apiextensions-apiserver v0.30.14/go.mod h1:NHLlcx7YmhxktP+eBfjxYPmsfnmsBU2Ue3/ytHzAHJE= k8s.io/apimachinery v0.30.14 h1:2OvEYwWoWeb25+xzFGP/8gChu+MfRNv24BlCQdnfGzQ= k8s.io/apimachinery v0.30.14/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/apiserver v0.30.14 h1:3iafln8nzOOShuTogncNiIM83FXxfqy3EaZMENjNn2o= +k8s.io/apiserver v0.30.14/go.mod h1:X1LOQEPPmPQ4pGg3wWRz4euhDK96gE2mzX3enFE+8PE= k8s.io/autoscaler/vertical-pod-autoscaler v1.2.2 h1:d6nrlgROIvGJrBZnmyTibA2CvXIylet/vBE1EicilRo= k8s.io/autoscaler/vertical-pod-autoscaler v1.2.2/go.mod h1:9ywHbt0kTrLyeNGgTNm7WEns34PmBMEr+9bDKTxW6wQ= k8s.io/client-go v0.30.14 h1:D81QZvBtv897JU4HRsx4YoaCDnzeZSvB8eApgmbtXVA= k8s.io/client-go v0.30.14/go.mod h1:9ytP3kKzrz3ZWavlWih4NB0mTdYA0DB1ElBHimq+JqQ= +k8s.io/component-base v0.30.14 h1:kDevqj2uEZLJTh8wCsEkpELPUwSRHV64h0zA7N0fe38= +k8s.io/component-base v0.30.14/go.mod h1:1MHb4dOuyJe0u61RO6xQYvZTtFaDg231WdC1agri2TE= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 h1:gAXU86Fmbr/ktY17lkHwSjw5aoThQvhnstGGIYKlKYc= k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911/go.mod h1:GLOk5B+hDbRROvt0X2+hqX64v/zO3vXN7J78OUmBSKw= k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 h1:/U5vjBbQn3RChhv7P11uhYvCSm5G2GaIi5AIGBS6r4c= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0/go.mod h1:z7+wmGM2dfIiLRfrC6jb5kV2Mq/sK1ZP303cxzkV5Y4= sigs.k8s.io/controller-runtime v0.18.7 h1:WDnx8LTRY8Fn1j/7B+S/R9MeDjWNAzpDBoaSvMSrQME= sigs.k8s.io/controller-runtime v0.18.7/go.mod h1:L9r3fUZhID7Q9eK9mseNskpaTg2n11f/tlb8odyzJ4Y= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= diff --git a/manifests/setup.yaml b/manifests/setup.yaml index 7d08070d0a..3016b7eb9b 100644 --- a/manifests/setup.yaml +++ b/manifests/setup.yaml @@ -1882,9 +1882,6 @@ spec: properties: compression: description: Compression enables compression of metrics collection data - enum: - - none - - gzip type: string credentials: description: |- @@ -1914,7 +1911,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' externalLabels: additionalProperties: type: string @@ -1923,9 +1921,6 @@ spec: data before being written to Google Cloud Monitoring or any other additional exports specified in the OperatorConfig. The precedence behavior matches that of Prometheus. type: object - x-kubernetes-validations: - - message: Invalid label key - rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) filter: description: Filter limits which metric data is sent to Cloud Monitoring (it doesn't apply to additional exports). properties: @@ -1975,6 +1970,7 @@ spec: properties: interval: description: The interval at which the metric endpoints are scraped. + format: duration type: string tlsInsecureSkipVerify: description: |- @@ -1994,13 +1990,14 @@ spec: properties: url: description: The URL of the endpoint that supports Prometheus Remote Write to export samples to. + format: uri type: string x-kubernetes-validations: - - rule: self == '' || isURL(self) + - message: url must be a valid URL + rule: self == '' || isURL(self) required: - url type: object - maxItems: 10 type: array features: description: Features holds configuration for optional managed-collection features. @@ -2013,9 +2010,6 @@ spec: Compression enables compression of the config data propagated by the operator to collectors and the rule-evaluator. It is recommended to use the gzip option when using a large number of ClusterPodMonitoring, PodMonitoring, GlobalRules, ClusterRules, and/or Rules. - enum: - - none - - gzip type: string type: object targetStatus: @@ -2066,7 +2060,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' externalURL: description: |- ExternalURL is the URL under which Alertmanager is externally reachable (for example, if @@ -2076,9 +2071,11 @@ spec: be derived automatically. If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page. + format: uri type: string x-kubernetes-validations: - - rule: self == '' || isURL(self) + - message: externalURL must be a valid URL + rule: self == '' || isURL(self) type: object metadata: type: object @@ -2126,7 +2123,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' type: description: |- Set the authentication type. Defaults to Bearer, Basic will cause an @@ -2135,13 +2133,9 @@ spec: type: object name: description: Name of Endpoints object in Namespace. - maxLength: 63 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string namespace: description: Namespace of Endpoints object. - maxLength: 63 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string pathPrefix: description: Prefix for the HTTP path alerts are pushed to. @@ -2154,12 +2148,10 @@ spec: x-kubernetes-int-or-string: true scheme: description: Scheme to use when firing alerts. - enum: - - http - - https type: string timeout: description: Timeout is a per-target Alertmanager timeout when pushing alerts. + format: duration type: string tls: description: TLS Config to use for alertmanager connection. @@ -2212,8 +2204,12 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' type: object + x-kubernetes-validations: + - message: SecretOrConfigMap fields are mutually exclusive + rule: '!(has(self.secret) && has(self.configMap))' cert: description: Struct containing the client cert file for the targets. properties: @@ -2262,8 +2258,12 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' type: object + x-kubernetes-validations: + - message: SecretOrConfigMap fields are mutually exclusive + rule: '!(has(self.secret) && has(self.configMap))' insecureSkipVerify: description: Disable target certificate validation. type: boolean @@ -2290,7 +2290,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' maxVersion: description: |- Maximum TLS version. Accepted values: TLS10 (TLS 1.0), TLS11 (TLS 1.1), TLS12 (TLS 1.2), TLS13 (TLS 1.3). @@ -2312,7 +2313,6 @@ spec: - namespace - port type: object - maxItems: 3 type: array type: object credentials: @@ -2344,7 +2344,8 @@ spec: type: object x-kubernetes-map-type: atomic x-kubernetes-validations: - - rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253 + - message: missing secret key selector name + rule: has(self.name) && self.name != '' externalLabels: additionalProperties: type: string @@ -2353,25 +2354,21 @@ spec: results and alerts produced by rules. The precedence behavior matches that of Prometheus. type: object - x-kubernetes-validations: - - message: Invalid label key - rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$')) generatorUrl: description: |- The base URL used for the generator URL in the alert notification payload. Should point to an instance of a query frontend that gives access to queryProjectID. + format: uri type: string x-kubernetes-validations: - - rule: self == '' || isURL(self) + - message: generatorUrl must be a valid URL + rule: self == '' || isURL(self) queryProjectID: description: |- QueryProjectID is the GCP project ID to evaluate rules against. If left blank, the rule-evaluator will try attempt to infer the Project ID from the environment. type: string - x-kubernetes-validations: - - message: Invalid GCP project ID - rule: self == '' || (self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') && size(self) >= 6 && size(self) <= 30) type: object scaling: description: Scaling contains configuration options for scaling GMP. diff --git a/pkg/operator/apis/monitoring/v1/operator_config_fuzz_test.go b/pkg/operator/apis/monitoring/v1/operator_config_fuzz_test.go new file mode 100644 index 0000000000..011b6f6c0d --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/operator_config_fuzz_test.go @@ -0,0 +1,312 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build goexperiment.jsonv2 + +package v1 + +import ( + "bytes" + json "encoding/json/v2" + "fmt" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/prometheus-engine/manifests" + apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel" + "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + celconfig "k8s.io/apiserver/pkg/apis/cel" +) + +func loadOperatorConfigSchema() (*apiextensionsv1.JSONSchemaProps, error) { + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifests.CRDManifest), 4096) + for { + var crd apiextensionsv1.CustomResourceDefinition + if err := decoder.Decode(&crd); err != nil { + break + } + if crd.Name == "operatorconfigs.monitoring.googleapis.com" { + if len(crd.Spec.Versions) == 0 { + return nil, fmt.Errorf("no versions found in OperatorConfig CRD") + } + return crd.Spec.Versions[0].Schema.OpenAPIV3Schema, nil + } + } + return nil, fmt.Errorf("OperatorConfig CRD not found in manifests") +} + +func TestInspectNewSchemaValidator(t *testing.T) { + apiSchema, err := loadOperatorConfigSchema() + if err != nil { + t.Fatalf("failed to load OperatorConfig schema: %v", err) + } + + var internalSchema apiextensions.JSONSchemaProps + err = apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(apiSchema, &internalSchema, nil) + if err != nil { + t.Fatalf("failed to convert schema: %v", err) + } + + structural, err := structuralschema.NewStructural(&internalSchema) + if err != nil { + t.Fatalf("failed to create structural schema: %v", err) + } + + celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit) + + invalidPayload := map[string]interface{}{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]interface{}{ + "name": "config", + "namespace": "gmp-public", + }, + "rules": map[string]interface{}{ + "alerting": map[string]interface{}{ + "alertmanagers": []interface{}{ + map[string]interface{}{ + "tls": map[string]interface{}{ + "ca": map[string]interface{}{ + "secret": map[string]interface{}{ + "name": "my-secret", + }, + "configMap": map[string]interface{}{ + "name": "my-configmap", + }, + }, + }, + }, + }, + }, + }, + } + + errs, _ := celValidator.Validate(t.Context(), nil, structural, invalidPayload, nil, celconfig.RuntimeCELCostBudget) + t.Logf("CEL Validation Errors: %v (len=%d)", errs, len(errs)) + if len(errs) == 0 { + t.Errorf("expected CEL validation to fail, but it passed!") + } +} + +// FuzzOperatorConfig runs differential fuzzing between the OpenAPIv3/CEL validations +// defined in the OperatorConfig CRD and the Go-based Webhook validation in OperatorConfig.Validate(). +func FuzzOperatorConfig(f *testing.F) { + apiSchema, err := loadOperatorConfigSchema() + if err != nil { + f.Fatalf("failed to load OperatorConfig schema: %v", err) + } + + var internalSchema apiextensions.JSONSchemaProps + if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(apiSchema, &internalSchema, nil); err != nil { + f.Fatalf("failed to convert schema: %v", err) + } + + // Compile the OpenAPI v3 schema validator + openapiValidator, _, err := validation.NewSchemaValidator(&internalSchema) + if err != nil { + f.Fatalf("failed to create OpenAPI validator: %v", err) + } + + // Compile the structural and CEL validator + structural, err := structuralschema.NewStructural(&internalSchema) + if err != nil { + f.Fatalf("failed to create structural schema: %v", err) + } + celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit) + + // Add seed corpus 1: A minimal valid OperatorConfig (no optional fields) + minimalSeed := map[string]interface{}{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]interface{}{ + "name": "config", + "namespace": "gmp-public", + }, + } + minimalSeedBytes, _ := json.Marshal(minimalSeed) + f.Add(minimalSeedBytes) + + // Add seed corpus 2: A fully populated valid OperatorConfig covering every possible field and nested subfield + fullyPopulatedSeed := map[string]interface{}{ + "apiVersion": "monitoring.googleapis.com/v1", + "kind": "OperatorConfig", + "metadata": map[string]interface{}{ + "name": "config", + "namespace": "gmp-public", + }, + "rules": map[string]interface{}{ + "queryProjectID": "my-gcp-project", + "generatorUrl": "https://prometheus.example.com", + "externalLabels": map[string]interface{}{ + "label_key": "label_val", + }, + "credentials": map[string]interface{}{ + "name": "rules-credentials", + "key": "key.json", + }, + "alerting": map[string]interface{}{ + "alertmanagers": []interface{}{ + map[string]interface{}{ + "namespace": "alertmanager-namespace", + "name": "alertmanager-name", + "port": 9093, + "scheme": "https", + "pathPrefix": "/api/v1", + "timeout": "10s", + "apiVersion": "v2", + "authorization": map[string]interface{}{ + "type": "Bearer", + "credentials": map[string]interface{}{ + "name": "auth-token-secret", + "key": "token", + }, + }, + "tls": map[string]interface{}{ + "ca": map[string]interface{}{ + "secret": map[string]interface{}{ + "name": "ca-secret", + "key": "ca.crt", + }, + }, + "cert": map[string]interface{}{ + "secret": map[string]interface{}{ + "name": "cert-secret", + "key": "tls.crt", + }, + }, + "keySecret": map[string]interface{}{ + "name": "key-secret", + "key": "tls.key", + }, + "serverName": "alertmanager.example.com", + "insecureSkipVerify": false, + }, + }, + }, + }, + }, + "collection": map[string]interface{}{ + "externalLabels": map[string]interface{}{ + "collection_label_key": "collection_label_val", + }, + "filter": map[string]interface{}{ + "matchOneOf": []interface{}{ + `{__name__=~"job:.*"}`, + }, + "enableMatchOneOf": true, + }, + "credentials": map[string]interface{}{ + "name": "collection-credentials", + "key": "key.json", + }, + "kubeletScraping": map[string]interface{}{ + "interval": "30s", + }, + "compression": "gzip", + }, + "exports": []interface{}{ + map[string]interface{}{ + "url": "https://remote-write-endpoint.example.com", + }, + }, + "managedAlertmanager": map[string]interface{}{ + "configSecret": map[string]interface{}{ + "name": "alertmanager", + "key": "alertmanager.yaml", + }, + "externalURL": "https://alertmanager-external.example.com", + }, + "features": map[string]interface{}{ + "targetStatus": map[string]interface{}{ + "enabled": true, + }, + }, + "scaling": map[string]interface{}{ + "vpa": map[string]interface{}{ + "enabled": true, + }, + }, + } + seedBytes, _ := json.Marshal(fullyPopulatedSeed) + f.Add(seedBytes) + + f.Fuzz(func(t *testing.T, data []byte) { + // 1. Structural check: Must unmarshal strictly into structured OperatorConfig (case-sensitive + no unknown fields) + var oc OperatorConfig + if err := json.Unmarshal(data, &oc, json.RejectUnknownMembers(true)); err != nil { + // Skip inputs that do not strictly match the OperatorConfig schema + t.Skip() + } + + // 2. Must unmarshal into unstructured map for schema/CEL validators + var unstructuredObj map[string]interface{} + if err := json.Unmarshal(data, &unstructuredObj); err != nil { + t.Skip() + } + + // 3. Execute OpenAPIv3 Schema Validation + openapiResult := openapiValidator.Validate(unstructuredObj) + if openapiResult.HasErrors() { + // If the object is structurally invalid according to the OpenAPI schema, + // the API server would reject it before it ever reaches the validating webhook or CEL rules. + // Therefore, we skip differential validation for this input. + t.Skip() + } + + // 4. Execute CEL Validation + celErrors, _ := celValidator.Validate(t.Context(), nil, structural, unstructuredObj, nil, celconfig.RuntimeCELCostBudget) + celPassed := len(celErrors) == 0 + + // 5. Execute Webhook Validation + webhookErr := oc.Validate() + webhookPassed := webhookErr == nil + + // 6. Differential assertion: + if celPassed != webhookPassed { + if !celPassed && webhookPassed { + if isURLValidationDiscrepancy(celErrors) { + t.Skip("Narrowly skipping: CEL is stricter than Webhook for URL validation") + } + t.Fatalf("Discrepancy (False Positive): CEL validation rejected the object, but Webhook accepted it.\nCEL Errors: %v\nPayload: %s", celErrors, string(data)) + } + if celPassed && !webhookPassed { + // This is a False Negative in CEL (CEL is too lenient / missing rules). + // We narrowly tolerate this if the webhook rejected it specifically because of generatorUrl parsing. + if strings.Contains(webhookErr.Error(), "failed to parse generator URL") { + t.Skip("Narrowly skipping: Webhook is stricter than CEL for generatorUrl validation") + } + t.Fatalf("Discrepancy (False Negative): CEL validation accepted the object, but Webhook rejected it.\nWebhook Error: %v\nPayload: %s", webhookErr, string(data)) + } + } + }) +} + +func isURLValidationDiscrepancy(errs field.ErrorList) bool { + if len(errs) == 0 { + return false + } + for _, err := range errs { + f := err.Field + isURLField := f == "rules.generatorUrl" || f == "managedAlertmanager.externalURL" || (strings.HasPrefix(f, "exports[") && strings.HasSuffix(f, "].url")) + if !isURLField { + return false + } + } + return true +} diff --git a/pkg/operator/apis/monitoring/v1/operator_types.go b/pkg/operator/apis/monitoring/v1/operator_types.go index d51145535e..3d6e0a7d71 100644 --- a/pkg/operator/apis/monitoring/v1/operator_types.go +++ b/pkg/operator/apis/monitoring/v1/operator_types.go @@ -40,7 +40,6 @@ type OperatorConfig struct { // Exports is an EXPERIMENTAL feature that specifies additional, optional endpoints to export to, // on top of Google Cloud Monitoring collection. // Note: To disable integrated export to Google Cloud Monitoring specify a non-matching filter in the "collection.filter" field. - // +kubebuilder:validation:MaxItems=10 Exports []ExportSpec `json:"exports,omitempty"` // ManagedAlertmanager holds information for configuring the managed instance of Alertmanager. // +kubebuilder:default={configSecret: {name: alertmanager, key: alertmanager.yaml}} @@ -147,16 +146,15 @@ type RuleEvaluatorSpec struct { // ExternalLabels specifies external labels that are attached to any rule // results and alerts produced by rules. The precedence behavior matches that // of Prometheus. - // +kubebuilder:validation:XValidation:rule="self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))",message="Invalid label key" ExternalLabels map[string]string `json:"externalLabels,omitempty"` // QueryProjectID is the GCP project ID to evaluate rules against. // If left blank, the rule-evaluator will try attempt to infer the Project ID // from the environment. - // +kubebuilder:validation:XValidation:rule="self == '' || (self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') && size(self) >= 6 && size(self) <= 30)",message="Invalid GCP project ID" QueryProjectID string `json:"queryProjectID,omitempty"` // The base URL used for the generator URL in the alert notification payload. // Should point to an instance of a query frontend that gives access to queryProjectID. - // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)" + // +kubebuilder:validation:Format=uri + // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)",message="generatorUrl must be a valid URL" GeneratorURL string `json:"generatorUrl,omitempty"` // Alerting contains how the rule-evaluator configures alerting. Alerting AlertingSpec `json:"alerting,omitempty"` @@ -166,7 +164,7 @@ type RuleEvaluatorSpec struct { // to which rule results are written. // Within GKE, this can typically be left empty if the compute default // service account has the required permissions. - // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="missing secret key selector name" Credentials *corev1.SecretKeySelector `json:"credentials,omitempty"` } @@ -175,7 +173,6 @@ type CollectionSpec struct { // ExternalLabels specifies external labels that are attached to all scraped // data before being written to Google Cloud Monitoring or any other additional exports // specified in the OperatorConfig. The precedence behavior matches that of Prometheus. - // +kubebuilder:validation:XValidation:rule="self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))",message="Invalid label key" ExternalLabels map[string]string `json:"externalLabels,omitempty"` // Filter limits which metric data is sent to Cloud Monitoring (it doesn't apply to additional exports). Filter ExportFilters `json:"filter,omitempty"` @@ -184,7 +181,7 @@ type CollectionSpec struct { // data is written. // Within GKE, this can typically be left empty if the compute default // service account has the required permissions. - // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="missing secret key selector name" Credentials *corev1.SecretKeySelector `json:"credentials,omitempty"` // Configuration to scrape the metric endpoints of the Kubelets. KubeletScraping *KubeletScraping `json:"kubeletScraping,omitempty"` @@ -194,7 +191,8 @@ type CollectionSpec struct { type ExportSpec struct { // The URL of the endpoint that supports Prometheus Remote Write to export samples to. - // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)" + // +kubebuilder:validation:Format=uri + // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)",message="url must be a valid URL" URL string `json:"url"` } @@ -221,7 +219,6 @@ type TargetStatusSpec struct { } // CompressionType is the compression type. -// +kubebuilder:validation:Enum=none;gzip type CompressionType string const ( @@ -232,6 +229,7 @@ const ( // KubeletScraping allows enabling scraping of the Kubelets' metric endpoints. type KubeletScraping struct { // The interval at which the metric endpoints are scraped. + // +kubebuilder:validation:Format=duration Interval string `json:"interval"` // TLSInsecureSkipVerify disables verifying the target cert. // This can be useful for clusters provisioned with kubeadm. @@ -280,7 +278,6 @@ type ExportFilters struct { // AlertingSpec defines alerting configuration. type AlertingSpec struct { // Alertmanagers contains endpoint configuration for designated Alertmanagers. - // +kubebuilder:validation:MaxItems=3 Alertmanagers []AlertmanagerEndpoints `json:"alertmanagers,omitempty"` } @@ -289,7 +286,7 @@ type AlertingSpec struct { type ManagedAlertmanagerSpec struct { // ConfigSecret refers to the name of a single-key Secret in the public namespace that // holds the managed Alertmanager config file. - // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="missing secret key selector name" ConfigSecret *corev1.SecretKeySelector `json:"configSecret,omitempty"` // ExternalURL is the URL under which Alertmanager is externally reachable (for example, if // Alertmanager is served via a reverse proxy). Used for generating relative and absolute @@ -298,7 +295,8 @@ type ManagedAlertmanagerSpec struct { // be derived automatically. // // If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page. - // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)" + // +kubebuilder:validation:Format=uri + // +kubebuilder:validation:XValidation:rule="self == '' || isURL(self)",message="externalURL must be a valid URL" ExternalURL string `json:"externalURL,omitempty"` } @@ -306,17 +304,12 @@ type ManagedAlertmanagerSpec struct { // containing alertmanager IPs to fire alerts against. type AlertmanagerEndpoints struct { // Namespace of Endpoints object. - // +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - // +kubebuilder:validation:MaxLength=63 Namespace string `json:"namespace"` // Name of Endpoints object in Namespace. - // +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - // +kubebuilder:validation:MaxLength=63 Name string `json:"name"` // Port the Alertmanager API is exposed on. Port intstr.IntOrString `json:"port"` // Scheme to use when firing alerts. - // +kubebuilder:validation:Enum=http;https Scheme string `json:"scheme,omitempty"` // Prefix for the HTTP path alerts are pushed to. PathPrefix string `json:"pathPrefix,omitempty"` @@ -328,6 +321,7 @@ type AlertmanagerEndpoints struct { // can be "v1" or "v2". APIVersion string `json:"apiVersion,omitempty"` // Timeout is a per-target Alertmanager timeout when pushing alerts. + // +kubebuilder:validation:Format=duration Timeout string `json:"timeout,omitempty"` } @@ -338,7 +332,7 @@ type Authorization struct { // error Type string `json:"type,omitempty"` // The secret's key that contains the credentials of the request - // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="missing secret key selector name" Credentials *corev1.SecretKeySelector `json:"credentials,omitempty"` } @@ -349,7 +343,7 @@ type TLSConfig struct { // Struct containing the client cert file for the targets. Cert *SecretOrConfigMap `json:"cert,omitempty"` // Secret containing the client key file for the targets. - // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="missing secret key selector name" KeySecret *corev1.SecretKeySelector `json:"keySecret,omitempty"` // Used to verify the hostname for the targets. ServerName string `json:"serverName,omitempty"` @@ -367,9 +361,10 @@ type TLSConfig struct { // SecretOrConfigMap allows to specify data as a Secret or ConfigMap. Fields are mutually exclusive. // Taking inspiration from prometheus-operator: https://github.com/prometheus-operator/prometheus-operator/blob/2c81b0cf6a5673e08057499a08ddce396b19dda4/Documentation/api.md#secretorconfigmap +// +kubebuilder:validation:XValidation:rule="!(has(self.secret) && has(self.configMap))",message="SecretOrConfigMap fields are mutually exclusive" type SecretOrConfigMap struct { // Secret containing data to use for the targets. - // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253" + // +kubebuilder:validation:XValidation:rule="has(self.name) && self.name != ''",message="missing secret key selector name" Secret *corev1.SecretKeySelector `json:"secret,omitempty"` // ConfigMap containing data to use for the targets. ConfigMap *corev1.ConfigMapKeySelector `json:"configMap,omitempty"` diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/0532d422a788de2f b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/0532d422a788de2f new file mode 100644 index 0000000000..c593bc97d8 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/0532d422a788de2f @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"alerting\":{\"alertmanagers\":[{}]}}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/243948b7fbb800b6 b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/243948b7fbb800b6 new file mode 100644 index 0000000000..d6c6540831 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/243948b7fbb800b6 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"collection\":{\"credentials\":{\"nAme\":\"0\"}}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/2586d20a23bef658 b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/2586d20a23bef658 new file mode 100644 index 0000000000..01c3917fec --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/2586d20a23bef658 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"apiVersion\":\"00\",\"metadata\":{},\"rules\":{\"generatorUrl\":\"// \"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3ec1a05a2fbed299 b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3ec1a05a2fbed299 new file mode 100644 index 0000000000..79de63d6d6 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3ec1a05a2fbed299 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"generatorUrl\":\"//[\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3f20a2b3c8d47659 b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3f20a2b3c8d47659 new file mode 100644 index 0000000000..34300d7b7a --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/3f20a2b3c8d47659 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"generatorUrl\":\"//^\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/4a50f58542ae9d7b b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/4a50f58542ae9d7b new file mode 100644 index 0000000000..9efdbcf524 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/4a50f58542ae9d7b @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"generatorUrl\":\"//0[\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/525880831c2ae64e b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/525880831c2ae64e new file mode 100644 index 0000000000..1a96473985 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/525880831c2ae64e @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"generatorUrl\":\"%0X\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5398a4ce76e5f19c b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5398a4ce76e5f19c new file mode 100644 index 0000000000..8fd555ecd6 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5398a4ce76e5f19c @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"generatorUrl\":\"//:A\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5b295d28608a74f3 b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5b295d28608a74f3 new file mode 100644 index 0000000000..7018c01248 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/5b295d28608a74f3 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"collection\":{\"compression\":\"0\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/7f42ed90ec4df77f b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/7f42ed90ec4df77f new file mode 100644 index 0000000000..21c8740459 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/7f42ed90ec4df77f @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"generatorUrl\":\"//>\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/8bb902370535cab5 b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/8bb902370535cab5 new file mode 100644 index 0000000000..57dd03c387 --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/8bb902370535cab5 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"exports\":[{}]}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/a41cfcf5df312c4a b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/a41cfcf5df312c4a new file mode 100644 index 0000000000..b3b2ced72b --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/a41cfcf5df312c4a @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"collection\":{\"compression\":\"\"}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/d3dff42ddb7b3b6e b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/d3dff42ddb7b3b6e new file mode 100644 index 0000000000..e43e7cec2f --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/d3dff42ddb7b3b6e @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"collection\":{\"kubeletScraping\":{\"interval\":\"A0s\"}}}") diff --git a/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/e567f7d4ad6ebe42 b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/e567f7d4ad6ebe42 new file mode 100644 index 0000000000..ecb4e06f1c --- /dev/null +++ b/pkg/operator/apis/monitoring/v1/testdata/fuzz/FuzzOperatorConfig/e567f7d4ad6ebe42 @@ -0,0 +1,2 @@ +go test fuzz v1 +[]byte("{\"rules\":{\"queryProjectID\":\"00000000000000\",\"generatorUrl\":\"A00000000000000000000000000000\",\"externalLabels\":{\"000000000\":\"000000000\"},\"credentials\":{\"name\":\"A\"}}}") diff --git a/pkg/operator/apis/monitoring/v1/zz_generated.deepcopy.go b/pkg/operator/apis/monitoring/v1/zz_generated.deepcopy.go index c6a5d372f2..53dd9e511b 100644 --- a/pkg/operator/apis/monitoring/v1/zz_generated.deepcopy.go +++ b/pkg/operator/apis/monitoring/v1/zz_generated.deepcopy.go @@ -587,6 +587,7 @@ func (in *GlobalRulesList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPClientConfig) DeepCopyInto(out *HTTPClientConfig) { *out = *in + out.ProxyConfig = in.ProxyConfig if in.Authorization != nil { in, out := &in.Authorization, &out.Authorization *out = new(Auth) @@ -607,7 +608,6 @@ func (in *HTTPClientConfig) DeepCopyInto(out *HTTPClientConfig) { *out = new(OAuth2) (*in).DeepCopyInto(*out) } - out.ProxyConfig = in.ProxyConfig return } @@ -718,6 +718,7 @@ func (in *MonitoringStatus) DeepCopy() *MonitoringStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OAuth2) DeepCopyInto(out *OAuth2) { *out = *in + out.ProxyConfig = in.ProxyConfig if in.ClientSecret != nil { in, out := &in.ClientSecret, &out.ClientSecret *out = new(SecretSelector) @@ -740,7 +741,6 @@ func (in *OAuth2) DeepCopyInto(out *OAuth2) { *out = new(TLS) (*in).DeepCopyInto(*out) } - out.ProxyConfig = in.ProxyConfig return } @@ -1299,6 +1299,7 @@ func (in *ScalingSpec) DeepCopy() *ScalingSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScrapeEndpoint) DeepCopyInto(out *ScrapeEndpoint) { *out = *in + in.HTTPClientConfig.DeepCopyInto(&out.HTTPClientConfig) out.Port = in.Port if in.Params != nil { in, out := &in.Params, &out.Params @@ -1322,7 +1323,6 @@ func (in *ScrapeEndpoint) DeepCopyInto(out *ScrapeEndpoint) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - in.HTTPClientConfig.DeepCopyInto(&out.HTTPClientConfig) return } From aba2056663621119d84a49062437edb1df6e3734 Mon Sep 17 00:00:00 2001 From: Adam Bernot Date: Fri, 26 Jun 2026 10:51:49 -0700 Subject: [PATCH 3/3] fix: reinstate pre-existing compression validation --- pkg/operator/apis/monitoring/v1/operator_types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/operator/apis/monitoring/v1/operator_types.go b/pkg/operator/apis/monitoring/v1/operator_types.go index 3d6e0a7d71..f6f839f0c7 100644 --- a/pkg/operator/apis/monitoring/v1/operator_types.go +++ b/pkg/operator/apis/monitoring/v1/operator_types.go @@ -219,6 +219,7 @@ type TargetStatusSpec struct { } // CompressionType is the compression type. +// +kubebuilder:validation:Enum=none;gzip type CompressionType string const (