Skip to content

Commit 95ec286

Browse files
google-labs-jules[bot]bernot-dev
authored andcommitted
feat: implement CEL validations for OperatorConfig
Adds kubebuilder CEL validations to `OperatorConfig` fields based on the migration away from webhook validation, per the provided design doc requirements. - 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 <bernot@google.com>
1 parent ee209b8 commit 95ec286

5 files changed

Lines changed: 158 additions & 0 deletions

File tree

charts/operator/crds/monitoring.googleapis.com_operatorconfigs.yaml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ spec:
7878
- key
7979
type: object
8080
x-kubernetes-map-type: atomic
81+
x-kubernetes-validations:
82+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
83+
&& size(self.name) <= 253
8184
externalLabels:
8285
additionalProperties:
8386
type: string
@@ -86,6 +89,9 @@ spec:
8689
data before being written to Google Cloud Monitoring or any other additional exports
8790
specified in the OperatorConfig. The precedence behavior matches that of Prometheus.
8891
type: object
92+
x-kubernetes-validations:
93+
- message: Invalid label key
94+
rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))
8995
filter:
9096
description: Filter limits which metric data is sent to Cloud Monitoring
9197
(it doesn't apply to additional exports).
@@ -157,9 +163,12 @@ spec:
157163
description: The URL of the endpoint that supports Prometheus Remote
158164
Write to export samples to.
159165
type: string
166+
x-kubernetes-validations:
167+
- rule: self == '' || isURL(self)
160168
required:
161169
- url
162170
type: object
171+
maxItems: 10
163172
type: array
164173
features:
165174
description: Features holds configuration for optional managed-collection
@@ -227,6 +236,9 @@ spec:
227236
- key
228237
type: object
229238
x-kubernetes-map-type: atomic
239+
x-kubernetes-validations:
240+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
241+
&& size(self.name) <= 253
230242
externalURL:
231243
description: |-
232244
ExternalURL is the URL under which Alertmanager is externally reachable (for example, if
@@ -237,6 +249,8 @@ spec:
237249
238250
If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page.
239251
type: string
252+
x-kubernetes-validations:
253+
- rule: self == '' || isURL(self)
240254
type: object
241255
metadata:
242256
type: object
@@ -288,6 +302,9 @@ spec:
288302
- key
289303
type: object
290304
x-kubernetes-map-type: atomic
305+
x-kubernetes-validations:
306+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
307+
&& size(self.name) <= 253
291308
type:
292309
description: |-
293310
Set the authentication type. Defaults to Bearer, Basic will cause an
@@ -296,9 +313,13 @@ spec:
296313
type: object
297314
name:
298315
description: Name of Endpoints object in Namespace.
316+
maxLength: 63
317+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
299318
type: string
300319
namespace:
301320
description: Namespace of Endpoints object.
321+
maxLength: 63
322+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
302323
type: string
303324
pathPrefix:
304325
description: Prefix for the HTTP path alerts are pushed
@@ -312,6 +333,9 @@ spec:
312333
x-kubernetes-int-or-string: true
313334
scheme:
314335
description: Scheme to use when firing alerts.
336+
enum:
337+
- http
338+
- https
315339
type: string
316340
timeout:
317341
description: Timeout is a per-target Alertmanager timeout
@@ -373,6 +397,9 @@ spec:
373397
- key
374398
type: object
375399
x-kubernetes-map-type: atomic
400+
x-kubernetes-validations:
401+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
402+
&& size(self.name) <= 253
376403
type: object
377404
cert:
378405
description: Struct containing the client cert file
@@ -427,6 +454,9 @@ spec:
427454
- key
428455
type: object
429456
x-kubernetes-map-type: atomic
457+
x-kubernetes-validations:
458+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
459+
&& size(self.name) <= 253
430460
type: object
431461
insecureSkipVerify:
432462
description: Disable target certificate validation.
@@ -456,6 +486,9 @@ spec:
456486
- key
457487
type: object
458488
x-kubernetes-map-type: atomic
489+
x-kubernetes-validations:
490+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
491+
&& size(self.name) <= 253
459492
maxVersion:
460493
description: |-
461494
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:
477510
- namespace
478511
- port
479512
type: object
513+
maxItems: 3
480514
type: array
481515
type: object
482516
credentials:
@@ -508,6 +542,9 @@ spec:
508542
- key
509543
type: object
510544
x-kubernetes-map-type: atomic
545+
x-kubernetes-validations:
546+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$')
547+
&& size(self.name) <= 253
511548
externalLabels:
512549
additionalProperties:
513550
type: string
@@ -516,16 +553,24 @@ spec:
516553
results and alerts produced by rules. The precedence behavior matches that
517554
of Prometheus.
518555
type: object
556+
x-kubernetes-validations:
557+
- message: Invalid label key
558+
rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))
519559
generatorUrl:
520560
description: |-
521561
The base URL used for the generator URL in the alert notification payload.
522562
Should point to an instance of a query frontend that gives access to queryProjectID.
523563
type: string
564+
x-kubernetes-validations:
565+
- rule: self == '' || isURL(self)
524566
queryProjectID:
525567
description: |-
526568
QueryProjectID is the GCP project ID to evaluate rules against.
527569
If left blank, the rule-evaluator will try attempt to infer the Project ID
528570
from the environment.
571+
maxLength: 30
572+
minLength: 6
573+
pattern: ^[a-z][a-z0-9-]*[a-z0-9]$
529574
type: string
530575
type: object
531576
scaling:

e2e/crd_validation_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import (
1919
"fmt"
2020
"os/exec"
2121
"path/filepath"
22+
"strings"
2223
"testing"
2324

2425
monitoringv1 "github.com/GoogleCloudPlatform/prometheus-engine/pkg/operator/apis/monitoring/v1"
2526
"github.com/google/go-cmp/cmp"
27+
corev1 "k8s.io/api/core/v1"
2628
v1 "k8s.io/api/core/v1"
2729
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2830
"k8s.io/apimachinery/pkg/util/intstr"
@@ -443,6 +445,7 @@ func TestCRDValidation(t *testing.T) {
443445
switch {
444446
case err == nil && !tc.wantErr:
445447
// OK
448+
_ = c.Delete(t.Context(), tc.obj)
446449
case err != nil && !tc.wantErr:
447450
t.Errorf("Unexpected error: %v", err)
448451
case err == nil && tc.wantErr:
@@ -791,6 +794,56 @@ func TestCRDValidation(t *testing.T) {
791794
},
792795
},
793796
},
797+
"valid credentials secret name (RFC 1123 subdomain)": {
798+
obj: &monitoringv1.OperatorConfig{
799+
ObjectMeta: metav1.ObjectMeta{
800+
Name: "config",
801+
Namespace: "gmp-public",
802+
},
803+
Collection: monitoringv1.CollectionSpec{
804+
Credentials: &corev1.SecretKeySelector{
805+
LocalObjectReference: corev1.LocalObjectReference{
806+
Name: "my-secret.v1",
807+
},
808+
Key: "key.json",
809+
},
810+
},
811+
},
812+
},
813+
"invalid credentials secret name": {
814+
obj: &monitoringv1.OperatorConfig{
815+
ObjectMeta: metav1.ObjectMeta{
816+
Name: "config",
817+
Namespace: "gmp-public",
818+
},
819+
Collection: monitoringv1.CollectionSpec{
820+
Credentials: &corev1.SecretKeySelector{
821+
LocalObjectReference: corev1.LocalObjectReference{
822+
Name: "my_secret",
823+
},
824+
Key: "key.json",
825+
},
826+
},
827+
},
828+
wantErr: true,
829+
},
830+
"credentials secret name too long": {
831+
obj: &monitoringv1.OperatorConfig{
832+
ObjectMeta: metav1.ObjectMeta{
833+
Name: "config",
834+
Namespace: "gmp-public",
835+
},
836+
Collection: monitoringv1.CollectionSpec{
837+
Credentials: &corev1.SecretKeySelector{
838+
LocalObjectReference: corev1.LocalObjectReference{
839+
Name: strings.Repeat("a", 254),
840+
},
841+
Key: "key.json",
842+
},
843+
},
844+
},
845+
wantErr: true,
846+
},
794847
}
795848
run(t, tests)
796849
})

manifests/fs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ import _ "embed"
2323
//go:embed operator.yaml
2424
var OperatorManifest []byte
2525

26+
// CRDManifest contains the OperatorConfig and GMP CRDs.
2627
//go:embed setup.yaml
2728
var CRDManifest []byte

manifests/setup.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,6 +1913,8 @@ spec:
19131913
- key
19141914
type: object
19151915
x-kubernetes-map-type: atomic
1916+
x-kubernetes-validations:
1917+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253
19161918
externalLabels:
19171919
additionalProperties:
19181920
type: string
@@ -1921,6 +1923,9 @@ spec:
19211923
data before being written to Google Cloud Monitoring or any other additional exports
19221924
specified in the OperatorConfig. The precedence behavior matches that of Prometheus.
19231925
type: object
1926+
x-kubernetes-validations:
1927+
- message: Invalid label key
1928+
rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))
19241929
filter:
19251930
description: Filter limits which metric data is sent to Cloud Monitoring (it doesn't apply to additional exports).
19261931
properties:
@@ -1990,9 +1995,12 @@ spec:
19901995
url:
19911996
description: The URL of the endpoint that supports Prometheus Remote Write to export samples to.
19921997
type: string
1998+
x-kubernetes-validations:
1999+
- rule: self == '' || isURL(self)
19932000
required:
19942001
- url
19952002
type: object
2003+
maxItems: 10
19962004
type: array
19972005
features:
19982006
description: Features holds configuration for optional managed-collection features.
@@ -2057,6 +2065,8 @@ spec:
20572065
- key
20582066
type: object
20592067
x-kubernetes-map-type: atomic
2068+
x-kubernetes-validations:
2069+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253
20602070
externalURL:
20612071
description: |-
20622072
ExternalURL is the URL under which Alertmanager is externally reachable (for example, if
@@ -2067,6 +2077,8 @@ spec:
20672077
20682078
If no URL is provided, Alertmanager will point to the Google Cloud Metric Explorer page.
20692079
type: string
2080+
x-kubernetes-validations:
2081+
- rule: self == '' || isURL(self)
20702082
type: object
20712083
metadata:
20722084
type: object
@@ -2113,6 +2125,8 @@ spec:
21132125
- key
21142126
type: object
21152127
x-kubernetes-map-type: atomic
2128+
x-kubernetes-validations:
2129+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253
21162130
type:
21172131
description: |-
21182132
Set the authentication type. Defaults to Bearer, Basic will cause an
@@ -2121,9 +2135,13 @@ spec:
21212135
type: object
21222136
name:
21232137
description: Name of Endpoints object in Namespace.
2138+
maxLength: 63
2139+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
21242140
type: string
21252141
namespace:
21262142
description: Namespace of Endpoints object.
2143+
maxLength: 63
2144+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
21272145
type: string
21282146
pathPrefix:
21292147
description: Prefix for the HTTP path alerts are pushed to.
@@ -2136,6 +2154,9 @@ spec:
21362154
x-kubernetes-int-or-string: true
21372155
scheme:
21382156
description: Scheme to use when firing alerts.
2157+
enum:
2158+
- http
2159+
- https
21392160
type: string
21402161
timeout:
21412162
description: Timeout is a per-target Alertmanager timeout when pushing alerts.
@@ -2190,6 +2211,8 @@ spec:
21902211
- key
21912212
type: object
21922213
x-kubernetes-map-type: atomic
2214+
x-kubernetes-validations:
2215+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253
21932216
type: object
21942217
cert:
21952218
description: Struct containing the client cert file for the targets.
@@ -2238,6 +2261,8 @@ spec:
22382261
- key
22392262
type: object
22402263
x-kubernetes-map-type: atomic
2264+
x-kubernetes-validations:
2265+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253
22412266
type: object
22422267
insecureSkipVerify:
22432268
description: Disable target certificate validation.
@@ -2264,6 +2289,8 @@ spec:
22642289
- key
22652290
type: object
22662291
x-kubernetes-map-type: atomic
2292+
x-kubernetes-validations:
2293+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253
22672294
maxVersion:
22682295
description: |-
22692296
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:
22852312
- namespace
22862313
- port
22872314
type: object
2315+
maxItems: 3
22882316
type: array
22892317
type: object
22902318
credentials:
@@ -2315,6 +2343,8 @@ spec:
23152343
- key
23162344
type: object
23172345
x-kubernetes-map-type: atomic
2346+
x-kubernetes-validations:
2347+
- rule: has(self.name) && self.name.matches('^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$') && size(self.name) <= 253
23182348
externalLabels:
23192349
additionalProperties:
23202350
type: string
@@ -2323,16 +2353,24 @@ spec:
23232353
results and alerts produced by rules. The precedence behavior matches that
23242354
of Prometheus.
23252355
type: object
2356+
x-kubernetes-validations:
2357+
- message: Invalid label key
2358+
rule: self.all(key, key.matches('^[a-zA-Z_][a-zA-Z0-9_]*$'))
23262359
generatorUrl:
23272360
description: |-
23282361
The base URL used for the generator URL in the alert notification payload.
23292362
Should point to an instance of a query frontend that gives access to queryProjectID.
23302363
type: string
2364+
x-kubernetes-validations:
2365+
- rule: self == '' || isURL(self)
23312366
queryProjectID:
23322367
description: |-
23332368
QueryProjectID is the GCP project ID to evaluate rules against.
23342369
If left blank, the rule-evaluator will try attempt to infer the Project ID
23352370
from the environment.
2371+
maxLength: 30
2372+
minLength: 6
2373+
pattern: ^[a-z][a-z0-9-]*[a-z0-9]$
23362374
type: string
23372375
type: object
23382376
scaling:

0 commit comments

Comments
 (0)