Skip to content

Commit 24fde7b

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

File tree

7 files changed

+240
-1
lines changed

7 files changed

+240
-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/v1alpha1
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/v1alpha1
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

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/v1alpha1=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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
166166
return ctrl.Result{RequeueAfter: time.Minute}, err
167167
}
168168

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

@@ -671,6 +677,75 @@ func (r *RuleReadinessController) cleanupNodesAfterSelectorChange(ctx context.Co
671677
return nil
672678
}
673679

680+
// syncTaintsConfigMap synchronizes readiness taints to a ConfigMap for admission policy.
681+
func (r *RuleReadinessController) syncTaintsConfigMap(ctx context.Context) error {
682+
log := ctrl.LoggerFrom(ctx)
683+
684+
// List all NodeReadinessRules
685+
var ruleList readinessv1alpha1.NodeReadinessRuleList
686+
if err := r.List(ctx, &ruleList); err != nil {
687+
return fmt.Errorf("failed to list NodeReadinessRules: %w", err)
688+
}
689+
690+
// Extract unique taint keys with readiness.k8s.io/ prefix and NoSchedule effect
691+
taintKeysSet := make(map[string]struct{})
692+
for _, rule := range ruleList.Items {
693+
if rule.Spec.Taint.Key != "" &&
694+
strings.HasPrefix(rule.Spec.Taint.Key, "readiness.k8s.io/") &&
695+
rule.Spec.Taint.Effect == corev1.TaintEffectNoSchedule {
696+
taintKeysSet[rule.Spec.Taint.Key] = struct{}{}
697+
}
698+
}
699+
700+
// Convert set to comma-separated string
701+
taintKeys := make([]string, 0, len(taintKeysSet))
702+
for key := range taintKeysSet {
703+
taintKeys = append(taintKeys, key)
704+
}
705+
taintKeysStr := strings.Join(taintKeys, ",")
706+
707+
// Update or create ConfigMap
708+
cm := &corev1.ConfigMap{
709+
ObjectMeta: metav1.ObjectMeta{
710+
Name: "readiness-taints",
711+
Namespace: "nrr-system",
712+
},
713+
}
714+
715+
// Try to get existing ConfigMap
716+
existingCM := &corev1.ConfigMap{}
717+
err := r.Get(ctx, client.ObjectKey{Name: "readiness-taints", Namespace: "nrr-system"}, existingCM)
718+
if err != nil && !apierrors.IsNotFound(err) {
719+
return fmt.Errorf("failed to get configmap: %w", err)
720+
}
721+
722+
// Set data
723+
cm.Data = map[string]string{
724+
"taint-keys": taintKeysStr,
725+
}
726+
727+
if apierrors.IsNotFound(err) {
728+
// Create new ConfigMap
729+
log.Info("Creating readiness-taints ConfigMap", "taintCount", len(taintKeys))
730+
if err := r.Create(ctx, cm); err != nil {
731+
return fmt.Errorf("failed to create configmap: %w", err)
732+
}
733+
} else {
734+
// Update existing ConfigMap
735+
log.V(1).Info("Updating readiness-taints ConfigMap", "taintCount", len(taintKeys))
736+
patch := client.MergeFrom(existingCM)
737+
existingCM.Data = cm.Data
738+
if err := r.Patch(ctx, existingCM, patch); err != nil {
739+
return fmt.Errorf("failed to update configmap: %w", err)
740+
}
741+
}
742+
743+
log.V(2).Info("Successfully synced taints to ConfigMap",
744+
"totalRules", len(ruleList.Items),
745+
"readinessTaints", len(taintKeys))
746+
return nil
747+
}
748+
674749
func (r *RuleReconciler) ensureFinalizer(ctx context.Context, rule *readinessv1alpha1.NodeReadinessRule, finalizer string) (finalizerAdded bool, err error) {
675750
// Finalizers can only be added when the deletionTimestamp is not set.
676751
if !rule.GetDeletionTimestamp().IsZero() {

0 commit comments

Comments
 (0)