Skip to content

Commit 4774fec

Browse files
authored
Support objectSelector on mutating webhook (#2058)
* feat: add support for setting objectSelector on webhook Signed-off-by: Cian Gallagher <[email protected]> * feat: update objectSelector to match expressions Signed-off-by: Cian Gallagher <[email protected]> * chore: use out of the box label parser Signed-off-by: Cian Gallagher <[email protected]> * chore: update chart version Signed-off-by: Cian Gallagher <[email protected]> * chore: update app version Signed-off-by: Cian Gallagher <[email protected]> * fix: use parseSelector Signed-off-by: Cian Gallagher <[email protected]> * ci: update minikube action to latest release Signed-off-by: Cian Gallagher <[email protected]> * revert: undo ci changes. create seperate pr Signed-off-by: Cian Gallagher <[email protected]> * Trigger CI Signed-off-by: Cian Gallagher <[email protected]> * chore: update chart version & docs following previous merge Signed-off-by: Cian Gallagher <[email protected]> * docs: update docs Signed-off-by: Cian Gallagher <[email protected]> --------- Signed-off-by: Cian Gallagher <[email protected]>
1 parent 0b67bae commit 4774fec

File tree

6 files changed

+119
-12
lines changed

6 files changed

+119
-12
lines changed

charts/spark-operator-chart/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
apiVersion: v2
22
name: spark-operator
33
description: A Helm chart for Spark on Kubernetes operator
4-
version: 1.4.1
5-
appVersion: v1beta2-1.6.0-3.5.0
4+
version: 1.4.2
5+
appVersion: v1beta2-1.6.1-3.5.0
66
keywords:
77
- spark
88
home: https://github.com/kubeflow/spark-operator

charts/spark-operator-chart/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# spark-operator
22

3-
![Version: 1.4.0](https://img.shields.io/badge/Version-1.4.0-informational?style=flat-square) ![AppVersion: v1beta2-1.6.0-3.5.0](https://img.shields.io/badge/AppVersion-v1beta2--1.6.0--3.5.0-informational?style=flat-square)
3+
![Version: 1.4.2](https://img.shields.io/badge/Version-1.4.2-informational?style=flat-square) ![AppVersion: v1beta2-1.6.1-3.5.0](https://img.shields.io/badge/AppVersion-v1beta2--1.6.1--3.5.0-informational?style=flat-square)
44

55
A Helm chart for Spark on Kubernetes operator
66

@@ -110,7 +110,7 @@ See [helm uninstall](https://helm.sh/docs/helm/helm_uninstall) for command docum
110110
| podMonitor.labels | object | `{}` | Pod monitor labels |
111111
| podMonitor.podMetricsEndpoint | object | `{"interval":"5s","scheme":"http"}` | Prometheus metrics endpoint properties. `metrics.portName` will be used as a port |
112112
| podSecurityContext | object | `{}` | Pod security context |
113-
| priorityClassName | string | `""` | Priority class to be used for running spark-operator pod. This helps in managing the pods during preemption. |
113+
| priorityClassName | string | `""` | A priority class to be used for running spark-operator pod. |
114114
| rbac.annotations | object | `{}` | Optional annotations for rbac |
115115
| rbac.create | bool | `false` | **DEPRECATED** use `createRole` and `createClusterRole` |
116116
| rbac.createClusterRole | bool | `true` | Create and use RBAC `ClusterRole` resources |
@@ -134,6 +134,7 @@ See [helm uninstall](https://helm.sh/docs/helm/helm_uninstall) for command docum
134134
| volumes | list | `[]` | |
135135
| webhook.enable | bool | `false` | Enable webhook server |
136136
| webhook.namespaceSelector | string | `""` | The webhook server will only operate on namespaces with this label, specified in the form key1=value1,key2=value2. Empty string (default) will operate on all namespaces |
137+
| webhook.objectSelector | string | `""` | The webhook will only operate on resources with this label/s, specified in the form key1=value1,key2=value2, OR key in (value1,value2). Empty string (default) will operate on all objects |
137138
| webhook.port | int | `8080` | Webhook service port |
138139
| webhook.portName | string | `"webhook"` | Webhook container port name and service target port name |
139140
| webhook.timeout | int | `30` | The annotations applied to init job, required to restore certs deleted by the cleanup job during upgrade |

charts/spark-operator-chart/templates/deployment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ spec:
100100
- -webhook-port={{ .Values.webhook.port }}
101101
- -webhook-timeout={{ .Values.webhook.timeout }}
102102
- -webhook-namespace-selector={{ .Values.webhook.namespaceSelector }}
103+
- -webhook-object-selector={{ .Values.webhook.objectSelector }}
103104
{{- end }}
104105
- -enable-resource-quota-enforcement={{ .Values.resourceQuotaEnforcement.enable }}
105106
{{- if gt (int .Values.replicaCount) 1 }}

charts/spark-operator-chart/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ webhook:
103103
# -- The webhook server will only operate on namespaces with this label, specified in the form key1=value1,key2=value2.
104104
# Empty string (default) will operate on all namespaces
105105
namespaceSelector: ""
106+
# -- The webhook will only operate on resources with this label/s, specified in the form key1=value1,key2=value2, OR key in (value1,value2).
107+
# Empty string (default) will operate on all objects
108+
objectSelector: ""
106109
# -- The annotations applied to init job, required to restore certs deleted by the cleanup job during upgrade
107110
timeout: 30
108111

pkg/webhook/webhook.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ type WebHook struct {
7878
serviceRef *arv1.ServiceReference
7979
failurePolicy arv1.FailurePolicyType
8080
selector *metav1.LabelSelector
81+
objectSelector *metav1.LabelSelector
8182
sparkJobNamespace string
8283
deregisterOnExit bool
8384
enableResourceQuotaEnforcement bool
@@ -96,6 +97,7 @@ type webhookFlags struct {
9697
webhookPort int
9798
webhookFailOnError bool
9899
webhookNamespaceSelector string
100+
webhookObjectSelector string
99101
}
100102

101103
var userConfig webhookFlags
@@ -109,6 +111,7 @@ func init() {
109111
flag.IntVar(&userConfig.webhookPort, "webhook-port", 8080, "Service port of the webhook server.")
110112
flag.BoolVar(&userConfig.webhookFailOnError, "webhook-fail-on-error", false, "Whether Kubernetes should reject requests when the webhook fails.")
111113
flag.StringVar(&userConfig.webhookNamespaceSelector, "webhook-namespace-selector", "", "The webhook will only operate on namespaces with this label, specified in the form key1=value1,key2=value2. Required if webhook-fail-on-error is true.")
114+
flag.StringVar(&userConfig.webhookObjectSelector, "webhook-object-selector", "", "The webhook will only operate on pods with this label/s, specified in the form key1=value1,key2=value2, OR key in (value1,value2).")
112115
}
113116

114117
// New creates a new WebHook instance.
@@ -119,8 +122,8 @@ func New(
119122
deregisterOnExit bool,
120123
enableResourceQuotaEnforcement bool,
121124
coreV1InformerFactory informers.SharedInformerFactory,
122-
webhookTimeout *int) (*WebHook, error) {
123-
125+
webhookTimeout *int,
126+
) (*WebHook, error) {
124127
certProvider, err := NewCertProvider(
125128
userConfig.webhookServiceName,
126129
userConfig.webhookServiceNamespace,
@@ -159,13 +162,21 @@ func New(
159162
return nil, fmt.Errorf("webhook-namespace-selector must be set when webhook-fail-on-error is true")
160163
}
161164
} else {
162-
selector, err := parseNamespaceSelector(userConfig.webhookNamespaceSelector)
165+
selector, err := parseSelector(userConfig.webhookNamespaceSelector)
163166
if err != nil {
164167
return nil, err
165168
}
166169
hook.selector = selector
167170
}
168171

172+
if userConfig.webhookObjectSelector != "" {
173+
selector, err := metav1.ParseToLabelSelector(userConfig.webhookObjectSelector)
174+
if err != nil {
175+
return nil, err
176+
}
177+
hook.objectSelector = selector
178+
}
179+
169180
if enableResourceQuotaEnforcement {
170181
hook.resourceQuotaEnforcer = resourceusage.NewResourceQuotaEnforcer(informerFactory, coreV1InformerFactory)
171182
}
@@ -180,7 +191,7 @@ func New(
180191
return hook, nil
181192
}
182193

183-
func parseNamespaceSelector(selectorArg string) (*metav1.LabelSelector, error) {
194+
func parseSelector(selectorArg string) (*metav1.LabelSelector, error) {
184195
selector := &metav1.LabelSelector{
185196
MatchLabels: make(map[string]string),
186197
}
@@ -189,7 +200,7 @@ func parseNamespaceSelector(selectorArg string) (*metav1.LabelSelector, error) {
189200
for _, selectorStr := range selectorStrs {
190201
kv := strings.SplitN(selectorStr, "=", 2)
191202
if len(kv) != 2 || kv[0] == "" || kv[1] == "" {
192-
return nil, fmt.Errorf("webhook namespace selector must be in the form key1=value1,key2=value2")
203+
return nil, fmt.Errorf("webhook selector must be in the form key1=value1,key2=value2")
193204
}
194205
selector.MatchLabels[kv[0]] = kv[1]
195206
}
@@ -441,6 +452,7 @@ func (wh *WebHook) selfRegistration(webhookConfigName string) error {
441452
},
442453
FailurePolicy: &wh.failurePolicy,
443454
NamespaceSelector: wh.selector,
455+
ObjectSelector: wh.objectSelector,
444456
TimeoutSeconds: wh.timeoutSeconds,
445457
SideEffects: &sideEffect,
446458
AdmissionReviewVersions: []string{"v1"},
@@ -455,6 +467,7 @@ func (wh *WebHook) selfRegistration(webhookConfigName string) error {
455467
},
456468
FailurePolicy: &wh.failurePolicy,
457469
NamespaceSelector: wh.selector,
470+
ObjectSelector: wh.objectSelector,
458471
TimeoutSeconds: wh.timeoutSeconds,
459472
SideEffects: &sideEffect,
460473
AdmissionReviewVersions: []string{"v1"},
@@ -587,7 +600,8 @@ func admitScheduledSparkApplications(review *admissionv1.AdmissionReview, enforc
587600
func mutatePods(
588601
review *admissionv1.AdmissionReview,
589602
lister crdlisters.SparkApplicationLister,
590-
sparkJobNs string) (*admissionv1.AdmissionResponse, error) {
603+
sparkJobNs string,
604+
) (*admissionv1.AdmissionResponse, error) {
591605
raw := review.Request.Object.Raw
592606
pod := &corev1.Pod{}
593607
if err := json.Unmarshal(raw, pod); err != nil {

pkg/webhook/webhook_test.go

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ import (
2323
"time"
2424

2525
"github.com/stretchr/testify/assert"
26-
2726
admissionv1 "k8s.io/api/admission/v1"
27+
arv1 "k8s.io/api/admissionregistration/v1"
2828
corev1 "k8s.io/api/core/v1"
2929
"k8s.io/apimachinery/pkg/api/equality"
3030
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3131
"k8s.io/apimachinery/pkg/runtime"
32+
"k8s.io/client-go/informers"
33+
"k8s.io/client-go/kubernetes/fake"
34+
gotest "k8s.io/client-go/testing"
3235

3336
spov1beta2 "github.com/kubeflow/spark-operator/pkg/apis/sparkoperator.k8s.io/v1beta2"
3437
crdclientfake "github.com/kubeflow/spark-operator/pkg/client/clientset/versioned/fake"
@@ -183,8 +186,93 @@ func serializePod(pod *corev1.Pod) ([]byte, error) {
183186
return json.Marshal(pod)
184187
}
185188

189+
func TestSelfRegistrationWithObjectSelector(t *testing.T) {
190+
clientset := fake.NewSimpleClientset()
191+
informerFactory := crdinformers.NewSharedInformerFactory(nil, 0)
192+
coreV1InformerFactory := informers.NewSharedInformerFactory(nil, 0)
193+
194+
// Setup userConfig with object selector
195+
userConfig.webhookObjectSelector = "spark-role in (driver,executor)"
196+
webhookTimeout := 30
197+
198+
// Create webhook instance
199+
webhook, err := New(clientset, informerFactory, "default", false, false, coreV1InformerFactory, &webhookTimeout)
200+
assert.NoError(t, err)
201+
202+
// Mock the clientset's Create function to capture the MutatingWebhookConfiguration object
203+
var createdWebhookConfig *arv1.MutatingWebhookConfiguration
204+
clientset.PrependReactor("create", "mutatingwebhookconfigurations", func(action gotest.Action) (handled bool, ret runtime.Object, err error) {
205+
createAction := action.(gotest.CreateAction)
206+
createdWebhookConfig = createAction.GetObject().(*arv1.MutatingWebhookConfiguration)
207+
return true, createdWebhookConfig, nil
208+
})
209+
210+
// Call the selfRegistration method
211+
err = webhook.selfRegistration("test-webhook-config")
212+
assert.NoError(t, err)
213+
214+
// Verify the MutatingWebhookConfiguration was created with the expected object selector
215+
assert.NotNil(t, createdWebhookConfig, "MutatingWebhookConfiguration should have been created")
216+
217+
expectedSelector := &metav1.LabelSelector{
218+
MatchExpressions: []metav1.LabelSelectorRequirement{
219+
{
220+
Key: "spark-role",
221+
Operator: metav1.LabelSelectorOpIn,
222+
Values: []string{"driver", "executor"},
223+
},
224+
},
225+
}
226+
actualSelector := createdWebhookConfig.Webhooks[0].ObjectSelector
227+
228+
assert.True(t, labelSelectorsEqual(expectedSelector, actualSelector), "ObjectSelectors should be equal")
229+
}
230+
231+
func labelSelectorsEqual(expected, actual *metav1.LabelSelector) bool {
232+
if expected == nil || actual == nil {
233+
return expected == nil && actual == nil
234+
}
235+
236+
if len(expected.MatchLabels) != len(actual.MatchLabels) {
237+
return false
238+
}
239+
240+
for k, v := range expected.MatchLabels {
241+
if actual.MatchLabels[k] != v {
242+
return false
243+
}
244+
}
245+
246+
if len(expected.MatchExpressions) != len(actual.MatchExpressions) {
247+
return false
248+
}
249+
250+
for i, expr := range expected.MatchExpressions {
251+
if expr.Key != actual.MatchExpressions[i].Key ||
252+
expr.Operator != actual.MatchExpressions[i].Operator ||
253+
!equalStringSlices(expr.Values, actual.MatchExpressions[i].Values) {
254+
return false
255+
}
256+
}
257+
258+
return true
259+
}
260+
261+
func equalStringSlices(a, b []string) bool {
262+
if len(a) != len(b) {
263+
return false
264+
}
265+
for i := range a {
266+
if a[i] != b[i] {
267+
return false
268+
}
269+
}
270+
return true
271+
}
272+
186273
func testSelector(input string, expected *metav1.LabelSelector, t *testing.T) {
187-
selector, err := parseNamespaceSelector(input)
274+
selector, err := parseSelector(input)
275+
188276
if expected == nil {
189277
if err == nil {
190278
t.Errorf("Expected error parsing '%s', but got %v", input, selector)

0 commit comments

Comments
 (0)