Skip to content

Commit cd89187

Browse files
committed
feat: auto tolerate daemonsets with MAP
Signed-off-by: pehlicd <furkanpehlivan34@gmail.com>
1 parent c206921 commit cd89187

File tree

8 files changed

+262
-1
lines changed

8 files changed

+262
-1
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
# MutatingAdmissionPolicyBinding binds the policy to the ConfigMap parameter
3+
apiVersion: admissionregistration.k8s.io/v1beta1
4+
kind: MutatingAdmissionPolicyBinding
5+
metadata:
6+
name: inject-daemonset-readiness-tolerations-binding
7+
spec:
8+
policyName: inject-daemonset-readiness-tolerations
9+
# Reference the ConfigMap containing toleration data
10+
paramRef:
11+
name: readiness-taints
12+
namespace: nrr-system
13+
parameterNotFoundAction: Deny
14+
matchResources:
15+
namespaceSelector: {}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
# ConfigMap that stores readiness tolerations
3+
# This will be populated/updated by the NodeReadinessRule controller
4+
apiVersion: v1
5+
kind: ConfigMap
6+
metadata:
7+
name: readiness-taints
8+
namespace: nrr-system
9+
data:
10+
# Store each toleration key separately for easier CEL access
11+
# Format: key1=readiness.k8s.io/NetworkReady,key2=readiness.k8s.io/StorageReady
12+
taint-keys: ""
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: kustomize.config.k8s.io/v1beta1
2+
kind: Kustomization
3+
4+
resources:
5+
- configmap.yaml
6+
- policy.yaml
7+
- binding.yaml
8+
9+
labels:
10+
- pairs:
11+
app.kubernetes.io/name: nrrcontroller
12+
app.kubernetes.io/component: admission-policy
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
# MutatingAdmissionPolicy for automatic DaemonSet toleration injection
3+
# Reads taint keys from a ConfigMap parameter resource
4+
# Requires: MutatingAdmissionPolicy feature enabled in the cluster
5+
apiVersion: admissionregistration.k8s.io/v1beta1
6+
kind: MutatingAdmissionPolicy
7+
metadata:
8+
name: inject-daemonset-readiness-tolerations
9+
spec:
10+
failurePolicy: Fail
11+
12+
# Define what this policy watches
13+
matchConstraints:
14+
resourceRules:
15+
- apiGroups: ["apps"]
16+
apiVersions: ["v1"]
17+
operations: ["CREATE", "UPDATE"]
18+
resources: ["daemonsets"]
19+
20+
# Reference the ConfigMap that contains toleration data
21+
paramKind:
22+
apiVersion: v1
23+
kind: ConfigMap
24+
25+
# Variables for CEL expressions
26+
variables:
27+
# Check if opt-out annotation is set
28+
- name: optedOut
29+
expression: |
30+
has(object.metadata.annotations) &&
31+
object.metadata.annotations.exists(k, k == "readiness.k8s.io/auto-tolerate" && object.metadata.annotations[k] == "false")
32+
33+
# Get existing tolerations (empty array if none)
34+
- name: existingTolerations
35+
expression: |
36+
has(object.spec.template.spec.tolerations) ?
37+
object.spec.template.spec.tolerations : []
38+
39+
# Get taint keys from ConfigMap and parse to array
40+
- name: taintKeys
41+
expression: |
42+
("taint-keys" in params.data) && params.data["taint-keys"] != "" ?
43+
params.data["taint-keys"].split(",") : []
44+
45+
# Create tolerations from taint keys (as plain maps since CEL has issues with complex types)
46+
- name: tolerationsToInject
47+
expression: |
48+
variables.taintKeys
49+
.filter(key, !variables.existingTolerations.exists(t, t.key == key))
50+
.map(key, {
51+
"key": key,
52+
"operator": "Exists",
53+
"effect": "NoSchedule"
54+
})
55+
56+
# Apply mutations
57+
mutations:
58+
- patchType: JSONPatch
59+
jsonPatch:
60+
expression: |
61+
!variables.optedOut && size(variables.tolerationsToInject) > 0 ?
62+
[
63+
JSONPatch{
64+
op: has(object.spec.template.spec.tolerations) ? "replace" : "add",
65+
path: "/spec/template/spec/tolerations",
66+
value: variables.existingTolerations + variables.tolerationsToInject
67+
}
68+
] : []
69+
70+
# Never reinvoke this policy
71+
reinvocationPolicy: Never

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ rules:
1414
- apiGroups:
1515
- ""
1616
resources:
17+
- events
18+
verbs:
19+
- create
20+
- patch
21+
- apiGroups:
22+
- ""
23+
resources:
24+
- configmaps
1725
- nodes
1826
verbs:
1927
- create

docs/admission-policy.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# MutatingAdmissionPolicy for DaemonSet Toleration Injection
2+
3+
This document describes how to deploy and use the MutatingAdmissionPolicy-based approach for automatically injecting readiness tolerations into DaemonSets.
4+
5+
## Overview
6+
7+
The MutatingAdmissionPolicy approach uses Kubernetes's native admission control mechanism with CEL (Common Expression Language) to inject tolerations **without running a webhook server**. This provides a simpler, more declarative alternative to the webhook-based approach.
8+
9+
## Requirements
10+
11+
> [!IMPORTANT]
12+
> MutatingAdmissionPolicy is needed to be enabled in the cluster.
13+
14+
- Feature gate: `MutatingAdmissionPolicy=true`
15+
- Runtime config: `admissionregistration.k8s.io/v1beta1=true`
16+
- `kubectl` configured to access your cluster
17+
- NodeReadinessRule CRDs installed
18+
19+
## Architecture
20+
21+
```
22+
User applies DaemonSet
23+
24+
API Server evaluates CEL policy
25+
26+
Fetches Tolerations ConfigMap which contains the tolerations to be injected
27+
28+
Injects tolerations (if applicable)
29+
30+
DaemonSet created with tolerations
31+
```
32+
33+
## Deployment
34+
35+
### Option 1: Using kustomize
36+
37+
```bash
38+
# Install CRDs first
39+
make install
40+
41+
# Deploy the admission policy
42+
kubectl apply -k config/admission-policy
43+
```
44+
45+
### Option 2: Direct kubectl apply
46+
47+
```bash
48+
# Install CRDs first
49+
make install
50+
51+
# Deploy policy and binding
52+
kubectl apply -f config/admission-policy/policy.yaml
53+
kubectl apply -f config/admission-policy/binding.yaml
54+
```

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
k8s.io/api v0.34.0
1111
k8s.io/apimachinery v0.34.0
1212
k8s.io/client-go v0.34.0
13+
k8s.io/klog/v2 v2.130.1
1314
sigs.k8s.io/controller-runtime v0.22.1
1415
)
1516

@@ -91,7 +92,6 @@ require (
9192
k8s.io/apiextensions-apiserver v0.34.0 // indirect
9293
k8s.io/apiserver v0.34.0 // indirect
9394
k8s.io/component-base v0.34.0 // indirect
94-
k8s.io/klog/v2 v2.130.1 // indirect
9595
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
9696
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
9797
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect

internal/controller/nodereadinessrule_controller.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func (r *RuleReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager)
9191
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/status,verbs=get;update;patch
9292
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/finalizers,verbs=update
9393
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
94+
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
95+
// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;create;update;patch;delete
9496

9597
func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
9698
log := ctrl.LoggerFrom(ctx)
@@ -167,13 +169,20 @@ func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
167169
return ctrl.Result{RequeueAfter: time.Minute}, err
168170
}
169171

172+
// Sync taints to ConfigMap for MutatingAdmissionPolicy
173+
if err := r.Controller.syncTaintsConfigMap(ctx); err != nil {
174+
log.Error(err, "Failed to sync taints configmap", "rule", rule.Name)
175+
// Don't fail reconciliation for this - log and continue
176+
}
177+
170178
return ctrl.Result{}, nil
171179
}
172180

173181
// reconcileDelete handles the rules deletion, It performs following actions
174182
// 1. Deletes the taints associated with the rule.
175183
// 2. Remove the rule from the cache.
176184
// 3. Remove the finalizer from the rule.
185+
// 4. Sync the Taints ConfigMap.
177186
func (r *RuleReconciler) reconcileDelete(ctx context.Context, rule *readinessv1alpha1.NodeReadinessRule) (ctrl.Result, error) {
178187
log := ctrl.LoggerFrom(ctx)
179188

@@ -197,6 +206,13 @@ func (r *RuleReconciler) reconcileDelete(ctx context.Context, rule *readinessv1a
197206
if err != nil {
198207
return ctrl.Result{}, err
199208
}
209+
210+
// Sync taints to ConfigMap for MutatingAdmissionPolicy
211+
if err := r.Controller.syncTaintsConfigMap(ctx); err != nil {
212+
log.Error(err, "Failed to sync taints configmap", "rule", rule.Name)
213+
// Don't fail reconciliation for this - log and continue
214+
}
215+
200216
return ctrl.Result{}, nil
201217
}
202218

@@ -678,6 +694,79 @@ func (r *RuleReadinessController) cleanupNodesAfterSelectorChange(ctx context.Co
678694
return nil
679695
}
680696

697+
// syncTaintsConfigMap synchronizes readiness taints to a ConfigMap for admission policy.
698+
func (r *RuleReadinessController) syncTaintsConfigMap(ctx context.Context) error {
699+
log := ctrl.LoggerFrom(ctx)
700+
701+
// List all NodeReadinessRules
702+
var ruleList readinessv1alpha1.NodeReadinessRuleList
703+
if err := r.List(ctx, &ruleList); err != nil {
704+
return fmt.Errorf("failed to list NodeReadinessRules: %w", err)
705+
}
706+
707+
// Extract unique taint keys with readiness.k8s.io/ prefix and NoSchedule effect
708+
taintKeysSet := make(map[string]struct{})
709+
for _, rule := range ruleList.Items {
710+
// Skip rules that are being deleted
711+
if !rule.DeletionTimestamp.IsZero() {
712+
continue
713+
}
714+
if rule.Spec.Taint.Key != "" &&
715+
strings.HasPrefix(rule.Spec.Taint.Key, "readiness.k8s.io/") &&
716+
rule.Spec.Taint.Effect == corev1.TaintEffectNoSchedule {
717+
taintKeysSet[rule.Spec.Taint.Key] = struct{}{}
718+
}
719+
}
720+
721+
// Convert set to comma-separated string
722+
taintKeys := make([]string, 0, len(taintKeysSet))
723+
for key := range taintKeysSet {
724+
taintKeys = append(taintKeys, key)
725+
}
726+
taintKeysStr := strings.Join(taintKeys, ",")
727+
728+
// Update or create ConfigMap
729+
cm := &corev1.ConfigMap{
730+
ObjectMeta: metav1.ObjectMeta{
731+
Name: "readiness-taints",
732+
Namespace: "nrr-system",
733+
},
734+
}
735+
736+
// Try to get existing ConfigMap
737+
existingCM := &corev1.ConfigMap{}
738+
err := r.Get(ctx, client.ObjectKey{Name: "readiness-taints", Namespace: "nrr-system"}, existingCM)
739+
if err != nil && !apierrors.IsNotFound(err) {
740+
return fmt.Errorf("failed to get configmap: %w", err)
741+
}
742+
743+
// Set data
744+
cm.Data = map[string]string{
745+
"taint-keys": taintKeysStr,
746+
}
747+
748+
if apierrors.IsNotFound(err) {
749+
// Create new ConfigMap
750+
log.Info("Creating readiness-taints ConfigMap", "taintCount", len(taintKeys))
751+
if err := r.Create(ctx, cm); err != nil {
752+
return fmt.Errorf("failed to create configmap: %w", err)
753+
}
754+
} else {
755+
// Update existing ConfigMap
756+
log.V(1).Info("Updating readiness-taints ConfigMap", "taintCount", len(taintKeys))
757+
patch := client.MergeFrom(existingCM.DeepCopy())
758+
existingCM.Data = cm.Data
759+
if err := r.Patch(ctx, existingCM, patch); err != nil {
760+
return fmt.Errorf("failed to update configmap: %w", err)
761+
}
762+
}
763+
764+
log.V(2).Info("Successfully synced taints to ConfigMap",
765+
"totalRules", len(ruleList.Items),
766+
"readinessTaints", len(taintKeys))
767+
return nil
768+
}
769+
681770
func (r *RuleReconciler) ensureFinalizer(ctx context.Context, rule *readinessv1alpha1.NodeReadinessRule, finalizer string) (finalizerAdded bool, err error) {
682771
// Finalizers can only be added when the deletionTimestamp is not set.
683772
if !rule.GetDeletionTimestamp().IsZero() {

0 commit comments

Comments
 (0)