Skip to content

Commit 6b7eb16

Browse files
committed
dangling service monitor
Signed-off-by: Tomasz Janiszewski <tomek@redhat.com>
1 parent e0ccfba commit 6b7eb16

File tree

9 files changed

+195
-43
lines changed

9 files changed

+195
-43
lines changed

docs/generated/templates.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ KubeLinter supports the following templates:
7575
**Parameters**:
7676

7777
```yaml
78-
- description: Check is a CEL expression used to validate the subject and objects.
78+
- description: 'Check contains a CEL expression for validation logic. Two predefined
79+
variables are available: ''object'' (the current Kubernetes object being processed)
80+
and ''objects'' (all objects being linted).'
7981
name: check
8082
negationAllowed: true
8183
regexAllowed: false

e2etests/bats-tests.sh

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,24 @@ get_value_from() {
2525
[ "$status" -eq 1 ]
2626

2727
message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message')
28-
failing_resource=$(get_value_from "${lines[0]}" '.Reports[1].Object.K8sObject.Name')
28+
message2=$(get_value_from "${lines[0]}" '.Reports[1].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[1].Diagnostic.Message')
29+
message3=$(get_value_from "${lines[0]}" '.Reports[2].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[2].Diagnostic.Message')
30+
message4=$(get_value_from "${lines[0]}" '.Reports[3].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[3].Diagnostic.Message')
31+
message5=$(get_value_from "${lines[0]}" '.Reports[4].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[4].Diagnostic.Message')
32+
message6=$(get_value_from "${lines[0]}" '.Reports[5].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[5].Diagnostic.Message')
33+
2934
count=$(get_value_from "${lines[0]}" '.Reports | length')
3035

36+
echo $message2
37+
3138
[[ "${message1}" == "Deployment: CEL check expression returned: Object has reloader annotation" ]]
32-
[[ "${failing_resource}" == "bad-irsa-role" ]]
33-
[[ "${count}" == "2" ]]
39+
[[ "${message2}" == "ServiceAccount: CEL check expression returned: Invalid EKS IAM role ARN format" ]]
40+
[[ "${message3}" == "ServiceMonitor: CEL check expression returned: no services found matching the service monitor's label selector and namespace selector" ]]
41+
[[ "${message4}" == "ServiceMonitor: CEL check expression returned: no services found matching the service monitor's label selector and namespace selector" ]]
42+
[[ "${message5}" == "ServiceMonitor: CEL check expression returned: no services found matching the service monitor's label selector and namespace selector" ]]
43+
[[ "${message6}" == "ServiceMonitor: CEL check expression returned: no services found matching the service monitor's label selector and namespace selector" ]]
44+
45+
[[ "${count}" == "6" ]]
3446
}
3547

3648
@test "template-check-installed-bash-version" {

e2etests/testdata/cel-config.yaml

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ checks:
33
customChecks:
44
- name: "cel-forbidden-annotation"
55
description: "cel espression cehck"
6-
remediation: "Remove reloader.stakater.com/auto annotation"
6+
remediation: "Remove foo.bar/baz annotation"
77
scope:
88
objectKinds:
99
- DeploymentLike
1010
template: "cel-expression"
1111
params:
1212
check: |
13-
has(subject.metadata.annotations) && subject.metadata.annotations["reloader.stakater.com/auto"] == "true" ? "Object has reloader annotation" : ""
13+
has(object.metadata.annotations) && object.metadata.annotations["foo.bar/baz"] == "true" ? "Object has reloader annotation" : ""
14+
1415
- name: invalid-irsa-role
1516
description: "IRSA annotations must have a valid IAM Role ARN value"
1617
remediation: "Validate the format of the annotation's value to ensure it is a valid IAM Role ARN"
@@ -20,4 +21,39 @@ customChecks:
2021
template: "cel-expression"
2122
params:
2223
check: |
23-
subject.metadata.annotations["eks.amazonaws.com/role-arn"].matches("^arn:aws:iam::\\d{12}:role/[\\w+=,.@-]{1,64}$") ? "" : "Invalid EKS IAM role ARN format"
24+
object.metadata.annotations["eks.amazonaws.com/role-arn"].matches("^arn:aws:iam::\\d{12}:role/[\\w+=,.@-]{1,64}$") ? "" : "Invalid EKS IAM role ARN format"
25+
26+
- name: "cel-dangling-servicemonitor"
27+
description: "Flag service monitors which do not match any service"
28+
remediation: "Ensure the ServiceMonitor's selector matches at least one Service"
29+
scope:
30+
objectKinds:
31+
- ServiceMonitor
32+
template: "cel-expression"
33+
params:
34+
check: |
35+
// Check if ServiceMonitor has selectors
36+
(!has(object.spec.selector.matchLabels) || size(object.spec.selector.matchLabels) == 0) &&
37+
(!has(object.spec.namespaceSelector.matchNames) || size(object.spec.namespaceSelector.matchNames) == 0) &&
38+
(!has(object.spec.namespaceSelector.any) || !object.spec.namespaceSelector.any) ?
39+
"service monitor has no selector specified" :
40+
41+
// Check if any services match the ServiceMonitor's selectors
42+
!objects.exists(obj,
43+
obj.kind == "Service" &&
44+
(
45+
// Check namespace selector
46+
(has(object.spec.namespaceSelector.any) && object.spec.namespaceSelector.any) ||
47+
(has(object.spec.namespaceSelector.matchNames) &&
48+
object.spec.namespaceSelector.matchNames.exists(ns, ns == (has(obj.metadata.namespace) ? obj.metadata.namespace : ""))) ||
49+
(!has(object.spec.namespaceSelector.matchNames) && !has(object.spec.namespaceSelector.any))
50+
) &&
51+
(
52+
// Check label selector
53+
(!has(object.spec.selector.matchLabels) || size(object.spec.selector.matchLabels) == 0) ||
54+
(has(obj.metadata.labels) &&
55+
object.spec.selector.matchLabels.all(key, key in obj.metadata.labels &&
56+
obj.metadata.labels[key] == object.spec.selector.matchLabels[key]))
57+
)
58+
) ?
59+
"no services found matching the service monitor's label selector and namespace selector" : ""

memoryrequirements.rego

Whitespace-only changes.

pkg/templates/cel/internal/params/gen-params.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/templates/cel/internal/params/params.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package params
22

3-
// Params represents the params accepted by this template.
3+
// Params defines the configuration parameters for this template.
44
type Params struct {
5-
// Check is a CEL expression used to validate the subject and objects.
5+
// Check contains a CEL expression for validation logic. Two predefined variables are available: 'object' (the current Kubernetes object being processed) and 'objects' (all objects being linted).
66
// +required
77
// +noregex
88
Check string

pkg/templates/cel/template.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,45 +47,45 @@ func init() {
4747
})
4848
}
4949

50-
func evaluate(check string, subject lintcontext.Object, objects []lintcontext.Object) (string, error) {
51-
// Convert subject to map via JSON marshaling/unmarshaling for CEL compatibility
50+
func evaluate(check string, object lintcontext.Object, objects []lintcontext.Object) (string, error) {
51+
// Convert object to map via JSON marshaling/unmarshaling for CEL compatibility
5252
// We need to marshal the underlying K8sObject, not the lintcontext.Object
53-
subjectMap, err := toMap(subject.K8sObject)
53+
objectMap, err := toMap(object.K8sObject)
5454
if err != nil {
55-
return "", err
55+
return "", fmt.Errorf("failed to convert object to map: %w", err)
5656
}
5757

5858
// Convert objects to maps via JSON marshaling/unmarshaling
5959
objectsMaps := make([]map[string]any, len(objects))
6060
for i, obj := range objects {
6161
objMap, err := toMap(obj.K8sObject)
6262
if err != nil {
63-
return "", err
63+
return "", fmt.Errorf("failed to convert object %s to map: %w", obj.GetK8sObjectName().String(), err)
6464
}
6565
objectsMaps[i] = objMap
6666
}
6767

6868
e, err := cel.NewEnv(
69-
cel.Variable("subject", cel.MapType(cel.StringType, cel.AnyType)),
69+
cel.Variable("object", cel.MapType(cel.StringType, cel.AnyType)),
7070
cel.Variable("objects", cel.ListType(cel.MapType(cel.StringType, cel.AnyType))),
7171
)
7272
if err != nil {
73-
return "", err
73+
return "", fmt.Errorf("failed to create CEL environment: %w", err)
7474
}
7575
ast, iss := e.Compile(check)
7676
if iss.Err() != nil {
77-
return "", iss.Err()
77+
return "", fmt.Errorf("failed to compile CEL expression: %w", iss.Err())
7878
}
7979
prg, err := e.Program(ast)
8080
if err != nil {
81-
return "", err
81+
return "", fmt.Errorf("failed to create CEL program: %w", err)
8282
}
8383
out, _, err := prg.Eval(map[string]any{
84-
"subject": subjectMap,
84+
"object": objectMap,
8585
"objects": objectsMaps,
8686
})
8787
if err != nil {
88-
return "", err
88+
return "", fmt.Errorf("failed to evaluate CEL expression: %w", err)
8989
}
9090

9191
o, ok := out.Value().(string)
@@ -98,11 +98,11 @@ func evaluate(check string, subject lintcontext.Object, objects []lintcontext.Ob
9898
func toMap(obj any) (map[string]any, error) {
9999
bytes, err := json.Marshal(obj)
100100
if err != nil {
101-
return nil, fmt.Errorf("failed to marshal subject: %w", err)
101+
return nil, fmt.Errorf("failed to marshal object: %w", err)
102102
}
103103
var output map[string]any
104104
if err := json.Unmarshal(bytes, &output); err != nil {
105-
return nil, fmt.Errorf("failed to unmarshal subject: %w", err)
105+
return nil, fmt.Errorf("failed to unmarshal object: %w", err)
106106
}
107107
return output, nil
108108
}

pkg/templates/cel/template_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ func TestEvaluate(t *testing.T) {
1313
tests := []struct {
1414
name string
1515
check string
16-
subject lintcontext.Object
16+
object lintcontext.Object
1717
objects []lintcontext.Object
1818
expectedMsg string
1919
expectError bool
2020
}{
2121
{
2222
name: "simple string return",
2323
check: `"test message"`,
24-
subject: lintcontext.Object{
24+
object: lintcontext.Object{
2525
K8sObject: &corev1.Pod{
2626
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
2727
},
@@ -31,9 +31,9 @@ func TestEvaluate(t *testing.T) {
3131
expectError: false,
3232
},
3333
{
34-
name: "conditional message based on subject",
35-
check: `subject.metadata.name == "test-pod" ? "pod found" : "pod not found"`,
36-
subject: lintcontext.Object{
34+
name: "conditional message based on object",
35+
check: `object.metadata.name == "test-pod" ? "pod found" : "pod not found"`,
36+
object: lintcontext.Object{
3737
K8sObject: &corev1.Pod{
3838
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
3939
},
@@ -45,7 +45,7 @@ func TestEvaluate(t *testing.T) {
4545
{
4646
name: "check objects list length",
4747
check: `size(objects) > 0 ? "objects found" : "no objects"`,
48-
subject: lintcontext.Object{
48+
object: lintcontext.Object{
4949
K8sObject: &corev1.Pod{
5050
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
5151
},
@@ -61,9 +61,9 @@ func TestEvaluate(t *testing.T) {
6161
expectError: false,
6262
},
6363
{
64-
name: "complex expression with subject properties",
65-
check: `subject.metadata.namespace == "default" && subject.metadata.name.startsWith("test") ? "valid test object" : "invalid object"`,
66-
subject: lintcontext.Object{
64+
name: "complex expression with object properties",
65+
check: `object.metadata.namespace == "default" && object.metadata.name.startsWith("test") ? "valid test object" : "invalid object"`,
66+
object: lintcontext.Object{
6767
K8sObject: &corev1.Pod{
6868
ObjectMeta: metav1.ObjectMeta{
6969
Name: "test-pod",
@@ -78,7 +78,7 @@ func TestEvaluate(t *testing.T) {
7878
{
7979
name: "empty string return",
8080
check: `""`,
81-
subject: lintcontext.Object{
81+
object: lintcontext.Object{
8282
K8sObject: &corev1.Pod{
8383
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
8484
},
@@ -90,7 +90,7 @@ func TestEvaluate(t *testing.T) {
9090
{
9191
name: "invalid CEL expression",
9292
check: `invalid syntax here`,
93-
subject: lintcontext.Object{
93+
object: lintcontext.Object{
9494
K8sObject: &corev1.Pod{
9595
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
9696
},
@@ -102,7 +102,7 @@ func TestEvaluate(t *testing.T) {
102102
{
103103
name: "non-string return type",
104104
check: `123`,
105-
subject: lintcontext.Object{
105+
object: lintcontext.Object{
106106
K8sObject: &corev1.Pod{
107107
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
108108
},
@@ -114,7 +114,7 @@ func TestEvaluate(t *testing.T) {
114114
{
115115
name: "boolean return type",
116116
check: `true`,
117-
subject: lintcontext.Object{
117+
object: lintcontext.Object{
118118
K8sObject: &corev1.Pod{
119119
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
120120
},
@@ -125,8 +125,8 @@ func TestEvaluate(t *testing.T) {
125125
},
126126
{
127127
name: "accessing nested properties",
128-
check: `subject.metadata.labels.app == "web" ? "web app detected" : "not a web app"`,
129-
subject: lintcontext.Object{
128+
check: `object.metadata.labels.app == "web" ? "web app detected" : "not a web app"`,
129+
object: lintcontext.Object{
130130
K8sObject: &corev1.Pod{
131131
ObjectMeta: metav1.ObjectMeta{
132132
Name: "test-pod",
@@ -144,7 +144,7 @@ func TestEvaluate(t *testing.T) {
144144

145145
for _, tt := range tests {
146146
t.Run(tt.name, func(t *testing.T) {
147-
msg, err := evaluate(tt.check, tt.subject, tt.objects)
147+
msg, err := evaluate(tt.check, tt.object, tt.objects)
148148

149149
if tt.expectError {
150150
assert.Error(t, err)

0 commit comments

Comments
 (0)