Skip to content

Commit 8d352bc

Browse files
authored
ODH Monitoring: Label boolean value validation admission policy (#3127)
* [+] odh monitoring label boolean value VAP/B ### Monitoring - Admission Components - Policies - Validations ### * [+] e2e odh monitoring label const * [+] odh monitoring label value enforcment e2e tests
1 parent 6ce7a4c commit 8d352bc

File tree

5 files changed

+219
-7
lines changed

5 files changed

+219
-7
lines changed

internal/controller/services/monitoring/monitoring_controller_actions.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
const (
2626
// Template files.
2727
MonitoringStackTemplate = "resources/monitoring-stack.tmpl.yaml"
28+
MonitoringAdmissionPoliciesTemplate = "resources/monitoring-admission-policies.tmpl.yaml"
2829
MonitoringStackAlertmanagerRBACTemplate = "resources/monitoringstack-alertmanager-rbac.tmpl.yaml"
2930
TempoMonolithicTemplate = "resources/tempo-monolithic.tmpl.yaml"
3031
TempoStackTemplate = "resources/tempo-stack.tmpl.yaml"
@@ -182,8 +183,20 @@ func updatePrometheusConfigMap(ctx context.Context, rr *odhtypes.ReconciliationR
182183
// Note: ValidatingAdmissionPolicy is a built-in Kubernetes API resource (not a CRD) available in K8s 1.26+.
183184
// If the API is not available, the controller setup will fail when trying to create the watch, providing
184185
// a clear error message to the user that their cluster doesn't support this feature.
185-
func deployMonitoringAdmissionPolicies(ctx context.Context, rr *odhtypes.ReconciliationRequest) error {
186-
// TODO: Implement admission policies deployment logic
186+
func deployMonitoringAdmissionPolicies(_ context.Context, rr *odhtypes.ReconciliationRequest) error {
187+
if _, ok := rr.Instance.(*serviceApi.Monitoring); !ok {
188+
return errors.New("instance is not of type *services.Monitoring")
189+
}
190+
191+
// no need to validate gvk of validation policy and binding because its a builtin type not a crd
192+
193+
// Deploy admission policy templates
194+
// If the ValidatingAdmissionPolicy API doesn't exist, deployment will fail with a clear error
195+
templates := []odhtypes.TemplateInfo{
196+
{FS: resourcesFS, Path: MonitoringAdmissionPoliciesTemplate},
197+
}
198+
199+
rr.Templates = append(rr.Templates, templates...)
187200
return nil
188201
}
189202

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Base admission policies that are always deployed
2+
# These validate that monitoring label values are boolean strings
3+
apiVersion: admissionregistration.k8s.io/v1
4+
kind: ValidatingAdmissionPolicy
5+
metadata:
6+
name: "monitoring-labels-values-validation"
7+
spec:
8+
failurePolicy: Fail
9+
matchConstraints:
10+
resourceRules:
11+
- apiGroups: ["monitoring.coreos.com"]
12+
apiVersions: ["v1"]
13+
operations: ["CREATE", "UPDATE"]
14+
resources: ["podmonitors", "servicemonitors"]
15+
- apiGroups: [""]
16+
apiVersions: ["v1"]
17+
operations: ["CREATE", "UPDATE"]
18+
resources: ["namespaces"]
19+
validations:
20+
- expression: >
21+
!has(object.metadata.labels) ||
22+
!('opendatahub.io/monitoring' in object.metadata.labels) ||
23+
object.metadata.labels['opendatahub.io/monitoring'] in ['true', 'false']
24+
message: "The label 'opendatahub.io/monitoring' must be set to 'true' or 'false'."
25+
26+
---
27+
apiVersion: admissionregistration.k8s.io/v1
28+
kind: ValidatingAdmissionPolicyBinding
29+
metadata:
30+
name: monitoring-labels-values-validation-binding
31+
spec:
32+
policyName: monitoring-labels-values-validation
33+
validationActions: ["Deny"]
34+

pkg/metadata/labels/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const (
1515
Platform = "platform"
1616
True = "true"
1717
CustomizedAppNamespace = "opendatahub.io/application-namespace"
18+
ODHLabelMonitoring = "opendatahub.io/monitoring"
1819
)
1920

2021
// K8SCommon keeps common kubernetes labels [1]

tests/e2e/monitoring_admission_components_test.go

Lines changed: 167 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"testing"
55

66
operatorv1 "github.com/openshift/api/operator/v1"
7+
corev1 "k8s.io/api/core/v1"
78
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
89
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
910
"k8s.io/apimachinery/pkg/types"
1011

1112
"github.com/opendatahub-io/opendatahub-operator/v2/internal/controller/status"
1213
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk"
14+
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels"
1315
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/utils/test/matchers/jq"
1416
"github.com/opendatahub-io/opendatahub-operator/v2/pkg/utils/test/testf"
1517

@@ -25,8 +27,6 @@ const (
2527
// createMonitorsEnvironment sets up the namespace and monitors with specific labels.
2628
// Pre-test cleanup: Ensures resources from previous test runs are deleted before creation.
2729
// Post-test: Respects --deletion-policy flag (cleanup handled by framework, not this function).
28-
//
29-
//nolint:unused // Helper function for future webhook tests - will be used when webhook validation tests are added
3030
func (tc *MonitoringTestCtx) createMonitorsEnvironment(t *testing.T, namespaceLabels map[string]string, monitorLabels map[string]string) {
3131
t.Helper()
3232

@@ -50,16 +50,16 @@ func (tc *MonitoringTestCtx) createMonitorsEnvironment(t *testing.T, namespaceLa
5050
t.Logf("Pre-test cleanup completed")
5151

5252
// Helper to apply metadata labels if provided
53-
applyLabels := func(labels map[string]string) testf.TransformFn {
53+
applyLabels := func(lbls map[string]string) testf.TransformFn {
5454
return func(obj *unstructured.Unstructured) error {
55-
if len(labels) == 0 {
55+
if len(lbls) == 0 {
5656
return nil
5757
}
5858
currentLabels := obj.GetLabels()
5959
if currentLabels == nil {
6060
currentLabels = make(map[string]string)
6161
}
62-
for k, v := range labels {
62+
for k, v := range lbls {
6363
currentLabels[k] = v
6464
}
6565
obj.SetLabels(currentLabels)
@@ -126,3 +126,165 @@ func (tc *MonitoringTestCtx) ValidateMonitoringWebhookTestsSetup(t *testing.T) {
126126

127127
t.Logf("Webhook tests setup complete: monitoring is enabled and ready")
128128
}
129+
130+
// ValidateMonitoringLabelValueEnforcementOnNamespace tests that the validation policy blocks invalid monitoring label values.
131+
func (tc *MonitoringTestCtx) ValidateMonitoringLabelValueEnforcementOnNamespace(t *testing.T) {
132+
t.Helper()
133+
134+
// Pre-test cleanup: ensure namespace doesn't exist from prior runs
135+
tc.DeleteResource(
136+
WithMinimalObject(gvk.Namespace, types.NamespacedName{Name: TestNamespaceName}),
137+
WithIgnoreNotFound(true),
138+
WithWaitForDeletion(true),
139+
)
140+
141+
// Attempt to create namespace with INVALID monitoring label value (not "true" or "false")
142+
invalidNamespace := &corev1.Namespace{
143+
ObjectMeta: metav1.ObjectMeta{
144+
Name: TestNamespaceName,
145+
Labels: map[string]string{
146+
labels.ODHLabelMonitoring: "invalid-value", // Invalid!
147+
},
148+
},
149+
}
150+
151+
// Expect this to be BLOCKED by validation policy
152+
err := tc.Client().Create(tc.Context(), invalidNamespace)
153+
tc.g.Expect(err).To(HaveOccurred(), "Validation policy should block namespace with invalid monitoring label value")
154+
tc.g.Expect(err).To(MatchError(ContainSubstring("must be set to 'true' or 'false'")), "Error message should indicate valid values")
155+
156+
// Explicit cleanup based on deletion policy (runs only if test reaches this point)
157+
switch testOpts.deletionPolicy {
158+
case DeletionPolicyAlways:
159+
t.Logf("Deletion Policy: Always. Cleaning up test namespace %s", TestNamespaceName)
160+
tc.DeleteResource(
161+
WithMinimalObject(gvk.Namespace, types.NamespacedName{Name: TestNamespaceName}),
162+
WithIgnoreNotFound(true),
163+
WithWaitForDeletion(true),
164+
)
165+
case DeletionPolicyOnFailure:
166+
if t.Failed() {
167+
t.Logf("Test failed. Deletion Policy: On Failure. Cleaning up test namespace %s", TestNamespaceName)
168+
tc.DeleteResource(
169+
WithMinimalObject(gvk.Namespace, types.NamespacedName{Name: TestNamespaceName}),
170+
WithIgnoreNotFound(true),
171+
WithWaitForDeletion(true),
172+
)
173+
}
174+
case DeletionPolicyNever:
175+
t.Logf("Deletion Policy: Never. Skipping cleanup of test namespace %s", TestNamespaceName)
176+
}
177+
}
178+
179+
// ValidateMonitoringLabelValueEnforcementOnMonitors tests that the validation policy blocks invalid monitoring label values.
180+
func (tc *MonitoringTestCtx) ValidateMonitoringLabelValueEnforcementOnMonitors(t *testing.T) {
181+
t.Helper()
182+
183+
// Pre-test cleanup: ensure invalid monitors don't exist from prior runs
184+
tc.DeleteResource(
185+
WithMinimalObject(gvk.CoreosPodMonitor, types.NamespacedName{Name: "test-invalid-podmonitor", Namespace: TestNamespaceName}),
186+
WithIgnoreNotFound(true),
187+
WithWaitForDeletion(true),
188+
)
189+
tc.DeleteResource(
190+
WithMinimalObject(gvk.CoreosServiceMonitor, types.NamespacedName{Name: "test-invalid-servicemonitor", Namespace: TestNamespaceName}),
191+
WithIgnoreNotFound(true),
192+
WithWaitForDeletion(true),
193+
)
194+
195+
// 1. Create a valid namespace first (validation usually requires the namespace to exist)
196+
tc.createMonitorsEnvironment(t, nil, nil) // Creates namespace & clean monitors. We will ignore the clean monitors.
197+
198+
// Define the invalid labels we want to test
199+
invalidLabels := map[string]string{
200+
labels.ODHLabelMonitoring: "invalid-value",
201+
}
202+
203+
// --- Test PodMonitor Validation ---
204+
205+
// Define an invalid PodMonitor object locally
206+
invalidPodMonitor := &unstructured.Unstructured{}
207+
invalidPodMonitor.SetGroupVersionKind(gvk.CoreosPodMonitor)
208+
invalidPodMonitor.SetName("test-invalid-podmonitor")
209+
invalidPodMonitor.SetNamespace(TestNamespaceName)
210+
invalidPodMonitor.SetLabels(invalidLabels)
211+
// Minimal valid spec so K8s doesn't reject it for schema reasons
212+
invalidPodMonitor.Object["spec"] = map[string]interface{}{
213+
"selector": map[string]interface{}{
214+
"matchLabels": map[string]interface{}{"app": "test"},
215+
},
216+
"podMetricsEndpoints": []interface{}{
217+
map[string]interface{}{"port": "metrics"},
218+
},
219+
}
220+
221+
// Attempt to create it - Expect Error
222+
err := tc.Client().Create(tc.Context(), invalidPodMonitor)
223+
tc.g.Expect(err).To(HaveOccurred(), "Validation policy should block PodMonitor with invalid monitoring label value")
224+
tc.g.Expect(err).To(MatchError(ContainSubstring("must be set to 'true' or 'false'")), "Error message should indicate valid values for PodMonitor")
225+
226+
// --- Test ServiceMonitor Validation ---
227+
228+
// Define an invalid ServiceMonitor object locally
229+
invalidServiceMonitor := &unstructured.Unstructured{}
230+
invalidServiceMonitor.SetGroupVersionKind(gvk.CoreosServiceMonitor)
231+
invalidServiceMonitor.SetName("test-invalid-servicemonitor")
232+
invalidServiceMonitor.SetNamespace(TestNamespaceName)
233+
invalidServiceMonitor.SetLabels(invalidLabels)
234+
// Minimal valid spec
235+
invalidServiceMonitor.Object["spec"] = map[string]interface{}{
236+
"selector": map[string]interface{}{
237+
"matchLabels": map[string]interface{}{"app": "test"},
238+
},
239+
"endpoints": []interface{}{
240+
map[string]interface{}{"port": "metrics"},
241+
},
242+
}
243+
244+
// Attempt to create it - Expect Error
245+
err = tc.Client().Create(tc.Context(), invalidServiceMonitor)
246+
tc.g.Expect(err).To(HaveOccurred(), "Validation policy should block ServiceMonitor with invalid monitoring label value")
247+
tc.g.Expect(err).To(MatchError(ContainSubstring("must be set to 'true' or 'false'")), "Error message should indicate valid values for ServiceMonitor")
248+
249+
// Explicit cleanup based on deletion policy (runs only if test reaches this point)
250+
switch testOpts.deletionPolicy {
251+
case DeletionPolicyAlways:
252+
t.Logf("Deletion Policy: Always. Cleaning up test resources in namespace %s", TestNamespaceName)
253+
tc.DeleteResource(
254+
WithMinimalObject(gvk.CoreosPodMonitor, types.NamespacedName{Name: "test-invalid-podmonitor", Namespace: TestNamespaceName}),
255+
WithIgnoreNotFound(true),
256+
WithWaitForDeletion(true),
257+
)
258+
tc.DeleteResource(
259+
WithMinimalObject(gvk.CoreosServiceMonitor, types.NamespacedName{Name: "test-invalid-servicemonitor", Namespace: TestNamespaceName}),
260+
WithIgnoreNotFound(true),
261+
WithWaitForDeletion(true),
262+
)
263+
tc.DeleteResource(
264+
WithMinimalObject(gvk.Namespace, types.NamespacedName{Name: TestNamespaceName}),
265+
WithIgnoreNotFound(true),
266+
WithWaitForDeletion(true),
267+
)
268+
case DeletionPolicyOnFailure:
269+
if t.Failed() {
270+
t.Logf("Test failed. Deletion Policy: On Failure. Cleaning up test resources in namespace %s", TestNamespaceName)
271+
tc.DeleteResource(
272+
WithMinimalObject(gvk.CoreosPodMonitor, types.NamespacedName{Name: "test-invalid-podmonitor", Namespace: TestNamespaceName}),
273+
WithIgnoreNotFound(true),
274+
WithWaitForDeletion(true),
275+
)
276+
tc.DeleteResource(
277+
WithMinimalObject(gvk.CoreosServiceMonitor, types.NamespacedName{Name: "test-invalid-servicemonitor", Namespace: TestNamespaceName}),
278+
WithIgnoreNotFound(true),
279+
WithWaitForDeletion(true),
280+
)
281+
tc.DeleteResource(
282+
WithMinimalObject(gvk.Namespace, types.NamespacedName{Name: TestNamespaceName}),
283+
WithIgnoreNotFound(true),
284+
WithWaitForDeletion(true),
285+
)
286+
}
287+
case DeletionPolicyNever:
288+
t.Logf("Deletion Policy: Never. Skipping cleanup of test resources in namespace %s", TestNamespaceName)
289+
}
290+
}

tests/e2e/monitoring_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ func monitoringTestSuite(t *testing.T) {
142142
if testOpts.webhookTest {
143143
testCases = append(testCases,
144144
TestCase{"Setup monitoring admission components tests", monitoringServiceCtx.ValidateMonitoringWebhookTestsSetup},
145+
TestCase{"Validate monitoring label value enforcement on namespace", monitoringServiceCtx.ValidateMonitoringLabelValueEnforcementOnNamespace},
146+
TestCase{"Validate monitoring label value enforcement on monitors", monitoringServiceCtx.ValidateMonitoringLabelValueEnforcementOnMonitors},
145147
)
146148
}
147149

0 commit comments

Comments
 (0)