Skip to content

Commit 35ab698

Browse files
committed
feat: add support for multiple CRs per namespace
Signed-off-by: Kevin Conner <[email protected]>
1 parent 9ee46c2 commit 35ab698

File tree

12 files changed

+180
-58
lines changed

12 files changed

+180
-58
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ The project is at an early stage and therefore has some limitations.
108108

109109
- There is no validation or defaulting for the custom resource.
110110
- The validation is namespace scoped and cannot be used across multiple namespaces.
111-
- No more than one validation resource can be used per namespace.
111+
112112
- There are no status fields for the custom resource.
113113
- The model and signature path must be specified, there is no auto discovery.
114114
- TLS certificates used by the webhook are self generated.
@@ -131,7 +131,7 @@ spec:
131131
signaturePath: /data/tensorflow_saved_model/model.sig
132132
```
133133
134-
All pods in the namespace where the custom resource exists that have this label `validation.ml.sigstore.dev/ml: "true"` will be validated.
134+
Pods in the namespace that have the label `validation.ml.sigstore.dev/ml: "<modelvalidation-cr-name>"` will be validated using the specified ModelValidation CR.
135135
It should be noted that this does not apply to subsequently labeled pods.
136136

137137
```diff
@@ -140,7 +140,7 @@ kind: Pod
140140
metadata:
141141
name: whatever-workload
142142
+ labels:
143-
+ validation.ml.sigstore.dev/ml: "true"
143+
+ validation.ml.sigstore.dev/ml: "demo"
144144
spec:
145145
restartPolicy: Never
146146
containers:

config/components/webhook/kustomization.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ patches:
3030
- op: add
3131
path: /webhooks/0/objectSelector
3232
value:
33-
matchLabels:
34-
validation.ml.sigstore.dev/ml: "true"
33+
matchExpressions:
34+
- key: "validation.ml.sigstore.dev/ml"
35+
operator: Exists
3536
target:
3637
kind: MutatingWebhookConfiguration
3738
name: mutating-webhook-configuration

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ kind: ClusterRole
44
metadata:
55
name: manager-role
66
rules:
7+
- apiGroups:
8+
- ""
9+
resources:
10+
- namespaces
11+
verbs:
12+
- get
13+
- list
14+
- watch
715
- apiGroups:
816
- ml.sigstore.dev
917
resources:

examples/unsigned.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ kind: Pod
2121
metadata:
2222
name: ollama
2323
labels:
24-
validation.ml.sigstore.dev/ml: "true"
24+
validation.ml.sigstore.dev/ml: "example"
2525
spec:
2626
containers:
2727
- name: ollama

examples/verify.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ kind: Pod
2323
metadata:
2424
name: whatever-workload
2525
labels:
26-
validation.ml.sigstore.dev/ml: "true"
26+
validation.ml.sigstore.dev/ml: "oss-na24-slasa-workshop"
2727
spec:
2828
# restartPolicy: Never
2929
containers:

internal/constants/labels.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package constants
2+
3+
const (
4+
// ModelValidationDomain is the domain used for model validation labels
5+
ModelValidationDomain = "validation.ml.sigstore.dev"
6+
7+
// ModelValidationLabel is the label used to enable model validation for a pod
8+
ModelValidationLabel = ModelValidationDomain + "/ml"
9+
10+
// IgnoreNamespaceLabel is the label used to ignore a namespace for model validation
11+
IgnoreNamespaceLabel = ModelValidationDomain + "/ignore"
12+
13+
// ModelValidationInitContainerName is the name of the init container injected for model validation
14+
ModelValidationInitContainerName = "model-validation"
15+
)

internal/webhooks/pod_webhook.go

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func NewPodInterceptor(c client.Client, decoder admission.Decoder) webhook.Admis
3030

3131
// +kubebuilder:rbac:groups=ml.sigstore.dev,resources=modelvalidations,verbs=get;list;watch
3232
// +kubebuilder:rbac:groups=ml.sigstore.dev,resources=modelvalidations/status,verbs=get;update;patch
33+
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
3334

3435
// podInterceptor extends pods with Model Validation Init-Container if annotation is specified.
3536
type podInterceptor struct {
@@ -54,36 +55,31 @@ func (p *podInterceptor) Handle(ctx context.Context, req admission.Request) admi
5455
logger.Error(err, "failed to get namespace")
5556
return admission.Errored(http.StatusInternalServerError, err)
5657
}
57-
if ns.Labels["validation.ml.sigstore.dev/ignore"] == "true" {
58+
if ns.Labels[constants.IgnoreNamespaceLabel] == "true" {
5859
logger.Info("Namespace has ignore label, skipping", "namespace", req.Namespace)
5960
return admission.Allowed("namespace ignored")
6061
}
6162

6263
logger.Info("Checking pod labels", "labels", pod.Labels)
63-
if v := pod.Labels["validation.ml.sigstore.dev/ml"]; v != "true" {
64-
logger.Info("Validation label not found or not true", "value", v)
65-
return admission.Allowed("no annotation found, no action needed")
64+
modelValidationName, ok := pod.Labels[constants.ModelValidationLabel]
65+
if !ok || modelValidationName == "" {
66+
logger.Info("ModelValidation label not found or empty, skipping injection")
67+
return admission.Allowed("no ModelValidation label found, no action needed")
6668
}
67-
logger.Info("Validation label found, proceeding with injection")
69+
logger.Info("ModelValidation label found, proceeding with injection", "modelValidationName", modelValidationName)
6870

69-
logger.Info("Search associated Model Validation CR", "pod", pod.Name, "namespace", pod.Namespace)
70-
rhmvList := &v1alpha1.ModelValidationList{}
71-
if err := p.client.List(ctx, rhmvList); err != nil {
72-
msg := "failed to get the ModelValidation Spec, skipping injection"
71+
logger.Info("Search associated Model Validation CR", "pod", pod.Name, "namespace", pod.Namespace,
72+
"modelValidationName", modelValidationName)
73+
rhmv := &v1alpha1.ModelValidation{}
74+
err := p.client.Get(ctx, client.ObjectKey{Name: modelValidationName, Namespace: pod.Namespace}, rhmv)
75+
if err != nil {
76+
msg := fmt.Sprintf("failed to get the ModelValidation CR %s/%s", pod.Namespace, modelValidationName)
7377
logger.Error(err, msg)
74-
return admission.Errored(http.StatusNotFound, err)
78+
return admission.Errored(http.StatusBadRequest, err) // Fail deployment if CR not found
7579
}
76-
77-
got := len(rhmvList.Items)
78-
if got != 1 {
79-
err := fmt.Errorf("got no or to many specs, expect: 1, got: %d", got)
80-
logger.Error(err, "skip injection")
81-
return admission.Errored(http.StatusBadRequest, err)
82-
}
83-
rhmv := rhmvList.Items[0]
8480
// NOTE: check if validation sidecar is already injected. Then no action needed.
8581
for _, c := range pod.Spec.InitContainers {
86-
if c.Name == modelValidationInitContainerName {
82+
if c.Name == constants.ModelValidationInitContainerName {
8783
return admission.Allowed("validation exists, no action needed")
8884
}
8985
}
@@ -98,7 +94,7 @@ func (p *podInterceptor) Handle(ctx context.Context, req admission.Request) admi
9894
vm = append(vm, c.VolumeMounts...)
9995
}
10096
pp.Spec.InitContainers = append(pp.Spec.InitContainers, corev1.Container{
101-
Name: modelValidationInitContainerName,
97+
Name: constants.ModelValidationInitContainerName,
10298
ImagePullPolicy: corev1.PullAlways,
10399
Image: constants.ModelTransparencyCliImage,
104100
Command: []string{"/usr/local/bin/model_signing"},
@@ -149,5 +145,3 @@ func validationConfigToArgs(logger logr.Logger, cfg v1alpha1.ValidationConfig, s
149145
logger.Info("missing validation config")
150146
return []string{}
151147
}
152-
153-
const modelValidationInitContainerName = "model-validation"

internal/webhooks/pod_webhook_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ var _ = Describe("Pod webhook", func() {
7171
ObjectMeta: metav1.ObjectMeta{
7272
Name: Name,
7373
Namespace: Namespace,
74-
Labels: map[string]string{"validation.ml.sigstore.dev/ml": "true"},
74+
Labels: map[string]string{constants.ModelValidationLabel: Name},
7575
},
7676
Spec: corev1.PodSpec{
7777
Containers: []corev1.Container{

test/e2e/e2e_suite_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ var (
4242
projectImage = "ghcr.io/sigstore/model-validation-operator:v0.0.1"
4343
)
4444

45+
const (
46+
// operatorNamespace is the namespace where the project is deployed in
47+
operatorNamespace = "model-validation-operator-system"
48+
49+
// webhookTestNamespace is the namespace for webhook tests
50+
webhookTestNamespace = "e2e-webhook-test-ns"
51+
)
52+
4553
// TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated,
4654
// temporary environment to validate project changes with the purposed to be used in CI jobs.
4755
// The default setup requires Kind, builds/loads the Manager Docker image locally, and installs

0 commit comments

Comments
 (0)