From 12de88dfe071198c951d126d4d28c06591d9cf03 Mon Sep 17 00:00:00 2001 From: Adam Bernot Date: Fri, 10 Apr 2026 17:21:18 +0000 Subject: [PATCH] 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"`