Skip to content

Commit 9676992

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

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
@@ -9,6 +9,7 @@ require (
99
k8s.io/api v0.34.0
1010
k8s.io/apimachinery v0.34.0
1111
k8s.io/client-go v0.34.0
12+
k8s.io/klog/v2 v2.130.1
1213
sigs.k8s.io/controller-runtime v0.22.1
1314
)
1415

@@ -65,7 +66,6 @@ require (
6566
gopkg.in/inf.v0 v0.9.1 // indirect
6667
gopkg.in/yaml.v3 v3.0.1 // indirect
6768
k8s.io/apiextensions-apiserver v0.34.0 // indirect
68-
k8s.io/klog/v2 v2.130.1 // indirect
6969
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
7070
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
7171
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect

internal/controller/nodereadinessrule_controller.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
164164
return ctrl.Result{RequeueAfter: time.Minute}, err
165165
}
166166

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

@@ -660,6 +666,75 @@ func (r *RuleReadinessController) cleanupNodesAfterSelectorChange(ctx context.Co
660666
return nil
661667
}
662668

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

0 commit comments

Comments
 (0)