@@ -159,6 +159,46 @@ type Config struct {
159159 // The URL configuration should be between quotes.
160160 // `url` cannot be specified when `path` is specified.
161161 URL string `marker:"url,optional"`
162+
163+ // NamespaceSelector decides whether to run the webhook on a request based on the namespace labels.
164+ // An object is selected if the namespace matches the selector.
165+ //
166+ // Examples:
167+ // // Match namespaces with a specific label
168+ // namespaceSelector=matchLabels~environment=production
169+ //
170+ // // Exclude the control-plane namespace (solves webhook bootstrap problem)
171+ // namespaceSelector=matchExpressions~key=control-plane.operator=DoesNotExist
172+ //
173+ // // Match multiple environments
174+ // namespaceSelector=matchExpressions~key=environment.operator=In.values=dev|staging|prod
175+ //
176+ // // Combine label match with expression
177+ // namespaceSelector=matchLabels~team=platform&matchExpressions~key=tier.operator=NotIn.values=system
178+ //
179+ // Operators: In, NotIn, Exists, DoesNotExist
180+ // Syntax: ~ separates selector type, . separates fields, & combines selectors, | separates values
181+ NamespaceSelector string `marker:"namespaceSelector,optional"`
182+
183+ // ObjectSelector decides whether to run the webhook on a request based on the object's labels.
184+ // An object is selected if it matches the selector.
185+ //
186+ // Examples:
187+ // // Only process objects managed by this operator
188+ // objectSelector=matchLabels~managed-by=my-operator
189+ //
190+ // // Only process production workloads
191+ // objectSelector=matchLabels~environment=production.tier=critical
192+ //
193+ // // Exclude objects with a specific label
194+ // objectSelector=matchExpressions~key=skip-validation.operator=DoesNotExist
195+ //
196+ // // Target specific application types
197+ // objectSelector=matchExpressions~key=app-type.operator=In.values=web|api|worker
198+ //
199+ // Operators: In, NotIn, Exists, DoesNotExist
200+ // Syntax: ~ separates selector type, . separates fields, & combines selectors, | separates values
201+ ObjectSelector string `marker:"objectSelector,optional"`
162202}
163203
164204// verbToAPIVariant converts a marker's verb to the proper value for the API.
@@ -180,6 +220,100 @@ func verbToAPIVariant(verbRaw string) admissionregv1.OperationType {
180220 }
181221}
182222
223+ // parseLabelSelector parses a label selector string into a metav1.LabelSelector.
224+ // Format examples:
225+ // - matchLabels: "matchLabels~key1=value1.key2=value2"
226+ // - matchExpressions: "matchExpressions~key=env.operator=In.values=dev|prod"
227+ // - combined: "matchLabels~key1=value1&matchExpressions~key=env.operator=In.values=dev|prod"
228+ func parseLabelSelector (selectorStr string ) (* metav1.LabelSelector , error ) {
229+ if selectorStr == "" {
230+ return nil , nil
231+ }
232+
233+ selector := & metav1.LabelSelector {}
234+
235+ // Split by ampersand to handle matchLabels and matchExpressions (if combined)
236+ for part := range strings .SplitSeq (selectorStr , "&" ) {
237+ part = strings .TrimSpace (part )
238+ if part == "" {
239+ continue
240+ }
241+
242+ // Check if this is a matchLabels part
243+ if labelsStr , ok := strings .CutPrefix (part , "matchLabels~" ); ok {
244+ if selector .MatchLabels == nil {
245+ selector .MatchLabels = make (map [string ]string )
246+ }
247+
248+ // Parse key=value pairs separated by dots
249+ for pair := range strings .SplitSeq (labelsStr , "." ) {
250+ pair = strings .TrimSpace (pair )
251+ if pair == "" {
252+ continue
253+ }
254+ kv := strings .SplitN (pair , "=" , 2 )
255+ if len (kv ) != 2 {
256+ return nil , fmt .Errorf ("invalid matchLabels format: %q, expected key=value" , pair )
257+ }
258+ selector .MatchLabels [strings .TrimSpace (kv [0 ])] = strings .TrimSpace (kv [1 ])
259+ }
260+ continue
261+ }
262+
263+ // Check if this is a matchExpressions part
264+ if exprStr , ok := strings .CutPrefix (part , "matchExpressions~" ); ok {
265+ // Parse the match expression fields separated by dots
266+ expr := & metav1.LabelSelectorRequirement {}
267+
268+ for field := range strings .SplitSeq (exprStr , "." ) {
269+ field = strings .TrimSpace (field )
270+ if field == "" {
271+ continue
272+ }
273+
274+ kv := strings .SplitN (field , "=" , 2 )
275+ if len (kv ) != 2 {
276+ return nil , fmt .Errorf ("invalid matchExpression field format: %q, expected key=value" , field )
277+ }
278+
279+ key := strings .TrimSpace (kv [0 ])
280+ value := strings .TrimSpace (kv [1 ])
281+
282+ switch key {
283+ case "key" :
284+ expr .Key = value
285+ case "operator" :
286+ expr .Operator = metav1 .LabelSelectorOperator (value )
287+ case "values" :
288+ expr .Values = strings .Split (value , "|" )
289+ default :
290+ return nil , fmt .Errorf ("unknown matchExpression field: %q" , key )
291+ }
292+ }
293+
294+ // Validate required fields
295+ if expr .Key == "" {
296+ return nil , fmt .Errorf ("matchExpression missing required 'key' field" )
297+ }
298+ if expr .Operator == "" {
299+ return nil , fmt .Errorf ("matchExpression missing required 'operator' field" )
300+ }
301+
302+ selector .MatchExpressions = append (selector .MatchExpressions , * expr )
303+ continue
304+ }
305+
306+ return nil , fmt .Errorf ("unexpected selector format: %q, expected matchLabels~ or matchExpressions~" , part )
307+ }
308+
309+ // Validate that we have at least one selector
310+ if selector .MatchLabels == nil && len (selector .MatchExpressions ) == 0 {
311+ return nil , fmt .Errorf ("label selector must have at least matchLabels or matchExpressions" )
312+ }
313+
314+ return selector , nil
315+ }
316+
183317// ToMutatingWebhookConfiguration converts this WebhookConfig to its Kubernetes API form.
184318func (c WebhookConfig ) ToMutatingWebhookConfiguration () (admissionregv1.MutatingWebhookConfiguration , error ) {
185319 if ! c .Mutating {
@@ -222,6 +356,16 @@ func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) {
222356 return admissionregv1.MutatingWebhook {}, err
223357 }
224358
359+ namespaceSelector , err := c .namespaceSelector ()
360+ if err != nil {
361+ return admissionregv1.MutatingWebhook {}, fmt .Errorf ("invalid namespaceSelector: %w" , err )
362+ }
363+
364+ objectSelector , err := c .objectSelector ()
365+ if err != nil {
366+ return admissionregv1.MutatingWebhook {}, fmt .Errorf ("invalid objectSelector: %w" , err )
367+ }
368+
225369 return admissionregv1.MutatingWebhook {
226370 Name : c .Name ,
227371 Rules : c .rules (),
@@ -232,6 +376,8 @@ func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) {
232376 TimeoutSeconds : c .timeoutSeconds (),
233377 AdmissionReviewVersions : c .AdmissionReviewVersions ,
234378 ReinvocationPolicy : c .reinvocationPolicy (),
379+ NamespaceSelector : namespaceSelector ,
380+ ObjectSelector : objectSelector ,
235381 }, nil
236382}
237383
@@ -251,6 +397,16 @@ func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error)
251397 return admissionregv1.ValidatingWebhook {}, err
252398 }
253399
400+ namespaceSelector , err := c .namespaceSelector ()
401+ if err != nil {
402+ return admissionregv1.ValidatingWebhook {}, fmt .Errorf ("invalid namespaceSelector: %w" , err )
403+ }
404+
405+ objectSelector , err := c .objectSelector ()
406+ if err != nil {
407+ return admissionregv1.ValidatingWebhook {}, fmt .Errorf ("invalid objectSelector: %w" , err )
408+ }
409+
254410 return admissionregv1.ValidatingWebhook {
255411 Name : c .Name ,
256412 Rules : c .rules (),
@@ -260,6 +416,8 @@ func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error)
260416 SideEffects : c .sideEffects (),
261417 TimeoutSeconds : c .timeoutSeconds (),
262418 AdmissionReviewVersions : c .AdmissionReviewVersions ,
419+ NamespaceSelector : namespaceSelector ,
420+ ObjectSelector : objectSelector ,
263421 }, nil
264422}
265423
@@ -402,6 +560,16 @@ func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType {
402560 return & reinvocationPolicy
403561}
404562
563+ // namespaceSelector returns the parsed namespaceSelector config for a webhook.
564+ func (c Config ) namespaceSelector () (* metav1.LabelSelector , error ) {
565+ return parseLabelSelector (c .NamespaceSelector )
566+ }
567+
568+ // objectSelector returns the parsed objectSelector config for a webhook.
569+ func (c Config ) objectSelector () (* metav1.LabelSelector , error ) {
570+ return parseLabelSelector (c .ObjectSelector )
571+ }
572+
405573// webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook.
406574func (c Config ) webhookVersions () ([]string , error ) {
407575 // If WebhookVersions is not specified, we default it to `v1`.
0 commit comments