Skip to content

feat: auto tolerate daemonsets with MAP#117

Open
pehlicd wants to merge 3 commits intokubernetes-sigs:mainfrom
pehlicd:feat-auto-tolerate-daemonSets-with-map
Open

feat: auto tolerate daemonsets with MAP#117
pehlicd wants to merge 3 commits intokubernetes-sigs:mainfrom
pehlicd:feat-auto-tolerate-daemonSets-with-map

Conversation

@pehlicd
Copy link
Contributor

@pehlicd pehlicd commented Feb 2, 2026

Description

This PR implements automatic DaemonSet toleration injection for readiness.k8s.io/* taints using MutatingAdmissionPolicy with ConfigMap parameter resource and automated sync via the existing controller.

Key features:

  • Zero additional infrastructure (no webhook server needed)
  • ConfigMap-based parameter resource for dynamic toleration data
  • Extended existing RuleReconciler to automatically sync taints → ConfigMap
  • CEL-based mutation logic
  • Opt-out support via annotation

Related Issue

Fixes #7

Type of Change

/kind feature

Testing

Tested in local kind cluster.

Checklist

  • make test passes
  • make lint passes

Does this PR introduce a user-facing change?

Added optional MutatingAdmissionPolicy for automatic DaemonSet toleration injection. When enabled, DaemonSets automatically receive tolerations for all readiness.k8s.io/* taints, ensuring they can schedule on nodes during readiness transitions. Requires Kubernetes 1.32+ with MutatingAdmissionPolicy feature gate enabled.

Doc #7

@k8s-ci-robot k8s-ci-robot added the kind/feature Categorizes issue or PR as related to a new feature. label Feb 2, 2026
@netlify
Copy link

netlify bot commented Feb 2, 2026

Deploy Preview for node-readiness-controller canceled.

Name Link
🔨 Latest commit 95191ab
🔍 Latest deploy log https://app.netlify.com/projects/node-readiness-controller/deploys/6988cfd2cbd71c0008fd998c

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: pehlicd
Once this PR has been reviewed and has the lgtm label, please assign tallclair for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Feb 2, 2026
Signed-off-by: pehlicd <furkanpehlivan34@gmail.com>
@pehlicd pehlicd force-pushed the feat-auto-tolerate-daemonSets-with-map branch from 9676992 to 24fde7b Compare February 2, 2026 11:24
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Feb 2, 2026
@Priyankasaggu11929
Copy link
Member

Priyankasaggu11929 commented Feb 8, 2026

Thank you so much for the PR @pehlicd.

I have left a few in-line comments.

In addition to those, how do you suggest handling RBAC for the controller to be able to update taints-key data in the readiness-taints configmap?
The PR currently miss that piece.

For my local testing, I manually created the required RBAC.


(updated later) Adding logs from my local testing, if you need to reproduce:

// configmap at the time of no node readiness rules in the cluster

❯ kubectl get configmap readiness-taints -n nrr-system -o yaml
apiVersion: v1
data:
  taint-keys: ""
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"taint-keys":""},"kind":"ConfigMap","metadata":{"annotations":{},"labels":{"app.kubernetes.io/component":"admission-policy","app.kubernetes.io/name":"nrrcontroller"},"name":"readiness-taints","namespace":"nrr-system"}}
  creationTimestamp: "2026-02-08T14:53:35Z"
  labels:
    app.kubernetes.io/component: admission-policy
    app.kubernetes.io/name: nrrcontroller
  name: readiness-taints
  namespace: nrr-system
  resourceVersion: "768"
  uid: 13bb93ad-c5d4-4aeb-b6d6-81a27f75f6f6

// created a node readiness rule
❯ kubectl apply -f examples/cni-readiness/network-readiness-rule.yaml
nodereadinessrule.readiness.node.x-k8s.io/network-readiness-rule created

// expected the configmap data to be updated with the taint from above nrr rule, but didn't work
❯ kubectl get configmap readiness-taints -n nrr-system -o yaml
apiVersion: v1
data:
  taint-keys: ""
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"taint-keys":""},"kind":"ConfigMap","metadata":{"annotations":{},"labels":{"app.kubernetes.io/component":"admission-policy","app.kubernetes.io/name":"nrrcontroller"},"name":"readiness-taints","namespace":"nrr-system"}}
  creationTimestamp: "2026-02-08T14:53:35Z"
  labels:
    app.kubernetes.io/component: admission-policy
    app.kubernetes.io/name: nrrcontroller
  name: readiness-taints
  namespace: nrr-system
  resourceVersion: "768"
  uid: 13bb93ad-c5d4-4aeb-b6d6-81a27f75f6f6

// logs shows permission errors
❯ kubectl logs -n nrr-system deployment/nrr-controller-manager --tail=30 | grep -i configmap
2026-02-08T14:54:04Z	ERROR	controller-runtime.cache.UnhandledError	Failed to watch	{"reflector": "pkg/mod/k8s.io/client-go@v0.34.0/tools/cache/reflector.go:290", "type": "*v1.ConfigMap", "error": "failed to list *v1.ConfigMap: configmaps is forbidden: User \"system:serviceaccount:nrr-system:nrr-controller-manager\" cannot list resource \"configmaps\" in API group \"\" at the cluster scope"}
2026-02-08T14:54:05Z	ERROR	controller-runtime.cache.UnhandledError	Failed to watch	{"reflector": "pkg/mod/k8s.io/client-go@v0.34.0/tools/cache/reflector.go:290", "type": "*v1.ConfigMap", "error": "failed to list *v1.ConfigMap: configmaps is forbidden: User \"system:serviceaccount:nrr-system:nrr-controller-manager\" cannot list resource \"configmaps\" in API group \"\" at the cluster scope"}
2026-02-08T14:54:08Z	ERROR	controller-runtime.cache.UnhandledError	Failed to watch	{"reflector": "pkg/mod/k8s.io/client-go@v0.34.0/tools/cache/reflector.go:290", "type": "*v1.ConfigMap", "error": "failed to list *v1.ConfigMap: configmaps is forbidden: User \"system:serviceaccount:nrr-system:nrr-controller-manager\" cannot list resource \"configmaps\" in API group \"\" at the cluster scope"}
2026-02-08T14:54:14Z	ERROR	controller-runtime.cache.UnhandledError	Failed to watch	{"reflector": "pkg/mod/k8s.io/client-go@v0.34.0/tools/cache/reflector.go:290", "type": "*v1.ConfigMap", "error": "failed to list *v1.ConfigMap: configmaps is forbidden: User \"system:serviceaccount:nrr-system:nrr-controller-manager\" cannot list resource \"configmaps\" in API group \"\" at the cluster scope"}

@Priyankasaggu11929
Copy link
Member

Also adding, I was able to test all the scenarios listed here:
#7 (comment)

(with the changes I suggested in the comments above)


In addition, I also tested deleting a node-readiness-rule -> which removes the corresponding taint from the readiness-taints configmap -> then I did a manual restart of a test daemonset which had matching tolerations -> controller didn't remove the matching toleration

which I believe is how it should be (we should not be removing any existing tolerations, only inject new ones - the MAP rule suggested in the PR achievss that)

@Priyankasaggu11929
Copy link
Member

/retest

Signed-off-by: pehlicd <furkanpehlivan34@gmail.com>
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;create;update;patch;delete
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although we had nodes rbac roles, it was missing in the inline comments so I added them.

Copy link
Member

@Priyankasaggu11929 Priyankasaggu11929 Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pehlicd, I looked into it further.

Firstly, the node resource RBAC added above - I don't think we need them at all for this PR's purpose. (correction - they're already added in the node controller here -

// +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=nodes/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core,resources=nodes/finalizers,verbs=update
).
So, Let's remove the redundant markers from here.

What I realised is we don't need the above cluster-scoped configmap RBAC as well -
because, we already have a namespace-scoped RBAC role for configmaps (leader-election-role) here which is better IMO in terms of surface area of permissions and serves our purpose -

rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete

The only problem is that currently the (controller-runtime's informer) cache is trying to watch configmaps at the cluster scope (see the error below again) but our namespaced role only provide permissions for nrr-system namespace, so I suggest we configure the cache to only watch configmaps in our required namespace.

❯ kubectl logs -n nrr-system 
   deployment/nrr-controller-manager --tail=30 | grep -i configmap
   2026-02-09T09:46:35Z    ERROR    controller-runtime.cache.UnhandledError    Failed 
   to watch{"reflector": 
   "pkg/mod/k8s.io/client-go@v0.34.0/tools/cache/reflector.go:290", "type": 
   "*v1.ConfigMap", "error": "failed to list *v1.ConfigMap: configmaps is forbidden: 
   User \"system:serviceaccount:nrr-system:nrr-controller-manager\" cannot list 
   resource \"configmaps\" in API group \"\" at the cluster scope"}

I tried like following, it worked -

diff --git a/cmd/main.go b/cmd/main.go
index 34652e9..1527845 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -28,11 +28,14 @@ import (
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
 	"k8s.io/client-go/rest"
 
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/client-go/kubernetes"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/cache"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/healthz"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
@@ -107,6 +110,15 @@ func main() {
 		HealthProbeBindAddress: probeAddr,
 		LeaderElection:         enableLeaderElection,
 		LeaderElectionID:       "ba65f13e.readiness.node.x-k8s.io",
+		Cache: cache.Options{
+			ByObject: map[client.Object]cache.ByObject{
+				&corev1.ConfigMap{}: {
+					Namespaces: map[string]cache.Config{
+						"nrr-system": {},
+					},
+				},
+			},
+		},
 	})
 	if err != nil {
 		setupLog.Error(err, "unable to start manager")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajaysundark, for your review as well ^

// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to those, how do you suggest handling RBAC for the controller to be able to update taints-key data in the readiness-taints configmap?
The PR currently miss that piece.

@Priyankasaggu11929 regarding to your comment above I added this inline kubebuilder comments and regenerated manifests.

@pehlicd
Copy link
Contributor Author

pehlicd commented Feb 8, 2026

Thanks for the detailed review, @Priyankasaggu11929. I believe I have addressed all the feedback. Please let me know if anything else is needed.

@pehlicd
Copy link
Contributor Author

pehlicd commented Feb 8, 2026

/retest

@k8s-ci-robot
Copy link
Contributor

@pehlicd: Cannot trigger testing until a trusted user reviews the PR and leaves an /ok-to-test message.

Details

In response to this:

/retest

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@ajaysundark
Copy link
Contributor

/retest

@ajaysundark
Copy link
Contributor

/ok-to-test

@k8s-ci-robot k8s-ci-robot added the ok-to-test Indicates a non-member PR verified by an org member that is safe to test. label Feb 8, 2026
Signed-off-by: pehlicd <furkanpehlivan34@gmail.com>
@pehlicd
Copy link
Contributor Author

pehlicd commented Feb 8, 2026

/retest

- name: taintKeys
expression: |
("taint-keys" in params.data) && params.data["taint-keys"] != "" ?
params.data["taint-keys"].split(",") : []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
params.data["taint-keys"].split(",") : []
params.data["taint-keys"].split(",").filter(key, key != "") : []

currently, if someone intentionally add a empty taint key to the configmap

our MAP policy will create a matching empty-key wildcard toleration

For example

❯ kubectl patch configmap readiness-taints -n nrr-system --type=merge   -p '{"data":{"taint-keys":"readiness.k8s.io/NetworkReady,,readiness.k8s.io/StorageReady"}}'
configmap/readiness-taints patched

// notice the two commas which basically mean an empty taint key
❯ kubectl get configmap -n nrr-system readiness-taints -o jsonpath='{.data.taint-keys}'
readiness.k8s.io/NetworkReady,,readiness.k8s.io/StorageReady


// the above empty taint, injected an empty-key wildcard toleration on the ds
❯ kubectl get ds test-ds -o jsonpath='{.spec.template.spec.tolerations[*]}' | jq 
{
  "effect": "NoSchedule",
  "key": "readiness.k8s.io/NetworkReady",
  "operator": "Exists"
}
{
  "effect": "NoSchedule",
  "operator": "Exists"
}
{
  "effect": "NoSchedule",
  "key": "readiness.k8s.io/StorageReady",
  "operator": "Exists"
}

paramRef:
name: readiness-taints
namespace: nrr-system
parameterNotFoundAction: Deny
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does parameterNotFoundAction:Deny blocks daemonset creations in the absence of this config-map param?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, in my local testing with the PR changes, it is blocking creation of new daemonsets.

It works if a user creates node-readiness-rules first and then daemonsets, but the other way around blocks the creation of new daemonsets.

I tested the behaviour by disabling the empty configmap file in the kustomization.yaml, because I was thinking why do we need to have an empty configmap file, if the configmap creation is taken care of by the controller itself - https://github.com/kubernetes-sigs/node-readiness-controller/pull/117/changes#diff-b857080def3b2fecd3967ca35b1bdc29564dda0b064fa902fa24efcd55471899R721-R727

But the thing is - the controller logic only kicks in when a user create a new node-readiness-rule.
So if a user created a daemonset first before creating the node-readiness-rules, that daemonset object creation is blocked.

The presence of the empty configmap file at the same time of creating MAP Policy/Policy-Binding fixes it.

❯ kubectl get node nrr-test-worker -o jsonpath={.spec.taints} | jq .
[
  {
    "effect": "NoSchedule",
    "key": "readiness.k8s.io/NetworkReady",
    "value": "pending"
  }
]

// applying MAP policy/policy-binding without empty configmap
❯ kubectl apply -k config/admission-policy
mutatingadmissionpolicy.admissionregistration.k8s.io/inject-daemonset-readiness-tolerations created
mutatingadmissionpolicybinding.admissionregistration.k8s.io/inject-daemonset-readiness-tolerations-binding created


❯ kubectl get cm -n nrr-system
NAME               DATA   AGE
kube-root-ca.crt   1      41s

// daemonset creation is blocked
❯ kubectl apply -f test-daemonset.yaml 
Error from server (Forbidden): error when creating "test-daemonset.yaml": policy 'inject-daemonset-readiness-tolerations' with binding 'inject-daemonset-readiness-tolerations-binding' denied request: failed to configure binding: no params found for policy binding with `Deny` parameterNotFoundAction

// creating a new nrr rule, which will trigger in turn configmap creation/patch per PR changes
❯ kubectl apply -f examples/cni-readiness/network-readiness-rule.yaml 
nodereadinessrule.readiness.node.x-k8s.io/network-readiness-rule created

❯ kubectl get cm readiness-taints -n nrr-system -o jsonpath={.data}
{"taint-keys":"readiness.k8s.io/NetworkReady"}


// now daemonset creation works
❯ kubectl apply -f test-daemonset.yaml 
daemonset.apps/test-ds created

Comment on lines 13 to 14
- create
- delete
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not from your changes, but dont think manager needs create or delete for any objects in core.

Comment on lines +44 to +54

### Option 2: Direct kubectl apply

```bash
# Install CRDs first
make install

# Deploy policy and binding
kubectl apply -f config/admission-policy/policy.yaml
kubectl apply -f config/admission-policy/binding.yaml
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. this looks redundant. also, dont we need to show updating the config-map and how we maintain it with readiness-taints introduced later?

Fetches Tolerations ConfigMap which contains the tolerations to be injected
Injects tolerations (if applicable)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesnt tell what is applicable. could we also add details on the annotation requirement?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/feature Categorizes issue or PR as related to a new feature. ok-to-test Indicates a non-member PR verified by an org member that is safe to test. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Auto tolerate DaemonSets with mutating admission controller

4 participants