istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/controllers/untaint/nodeuntainter.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package untaint 16 17 import ( 18 "encoding/json" 19 "fmt" 20 21 "gomodules.xyz/jsonpatch/v2" 22 v1 "k8s.io/api/core/v1" 23 "k8s.io/apimachinery/pkg/types" 24 25 "istio.io/istio/pkg/config/labels" 26 kubelib "istio.io/istio/pkg/kube" 27 "istio.io/istio/pkg/kube/controllers" 28 "istio.io/istio/pkg/kube/kclient" 29 "istio.io/istio/pkg/kube/krt" 30 "istio.io/istio/pkg/kube/kubetypes" 31 istiolog "istio.io/istio/pkg/log" 32 ) 33 34 var log = istiolog.RegisterScope("untaint", "CNI node-untaint controller") 35 36 const ( 37 TaintName = "cni.istio.io/not-ready" 38 ) 39 40 var istioCniLabels = map[string]string{ 41 "k8s-app": "istio-cni-node", 42 } 43 44 type NodeUntainter struct { 45 podsClient kclient.Client[*v1.Pod] 46 nodesClient kclient.Client[*v1.Node] 47 cnilabels labels.Instance 48 ourNs string 49 queue controllers.Queue 50 } 51 52 func filterNamespace(ns string) func(any) bool { 53 return func(obj any) bool { 54 object := controllers.ExtractObject(obj) 55 if object == nil { 56 return false 57 } 58 return ns == object.GetNamespace() 59 } 60 } 61 62 func NewNodeUntainter(stop <-chan struct{}, kubeClient kubelib.Client, cniNs, sysNs string) *NodeUntainter { 63 log.Debugf("starting node untainter with labels %v", istioCniLabels) 64 ns := cniNs 65 if ns == "" { 66 ns = sysNs 67 } 68 podsClient := kclient.NewFiltered[*v1.Pod](kubeClient, kclient.Filter{ 69 ObjectFilter: kubetypes.NewStaticObjectFilter(filterNamespace(ns)), 70 ObjectTransform: kubelib.StripPodUnusedFields, 71 }) 72 nodes := kclient.NewFiltered[*v1.Node](kubeClient, kclient.Filter{ObjectTransform: kubelib.StripNodeUnusedFields}) 73 nt := &NodeUntainter{ 74 podsClient: podsClient, 75 nodesClient: nodes, 76 cnilabels: labels.Instance(istioCniLabels), 77 ourNs: ns, 78 } 79 nt.setup(stop) 80 return nt 81 } 82 83 func (n *NodeUntainter) setup(stop <-chan struct{}) { 84 nodes := krt.WrapClient[*v1.Node](n.nodesClient) 85 pods := krt.WrapClient[*v1.Pod](n.podsClient) 86 87 readyCniPods := krt.NewCollection(pods, func(ctx krt.HandlerContext, p *v1.Pod) *v1.Pod { 88 log.Debugf("cniPods event: %s", p.Name) 89 if p.Namespace != n.ourNs { 90 return nil 91 } 92 if !n.cnilabels.SubsetOf(p.ObjectMeta.Labels) { 93 return nil 94 } 95 if !IsPodReadyConditionTrue(p.Status) { 96 return nil 97 } 98 log.Debugf("pod %s on node %s ready!", p.Name, p.Spec.NodeName) 99 return p 100 }, krt.WithStop(stop)) 101 102 // these are all the nodes that have a ready cni pod. if the cni pod is ready, 103 // it means we are ok scheduling pods to it. 104 readyCniNodes := krt.NewCollection(readyCniPods, func(ctx krt.HandlerContext, p v1.Pod) *v1.Node { 105 pnode := krt.FetchOne(ctx, nodes, krt.FilterKey(p.Spec.NodeName)) 106 if pnode == nil { 107 return nil 108 } 109 node := *pnode 110 if !hasTaint(node) { 111 return nil 112 } 113 return node 114 }, krt.WithStop(stop)) 115 116 n.queue = controllers.NewQueue("untaint nodes", 117 controllers.WithReconciler(n.reconcileNode), 118 controllers.WithMaxAttempts(5)) 119 120 // remove the taints from readyCniNodes 121 readyCniNodes.Register(func(o krt.Event[v1.Node]) { 122 if o.Event == controllers.EventDelete { 123 return 124 } 125 if o.New != nil { 126 log.Debugf("adding node to queue event: %s", o.New.Name) 127 n.queue.AddObject(o.New) 128 } 129 }) 130 } 131 132 func (n *NodeUntainter) HasSynced() bool { 133 return n.queue.HasSynced() 134 } 135 136 func (n *NodeUntainter) Run(stop <-chan struct{}) { 137 kubelib.WaitForCacheSync("node untainer", stop, n.nodesClient.HasSynced, n.podsClient.HasSynced) 138 n.queue.Run(stop) 139 n.podsClient.ShutdownHandlers() 140 n.nodesClient.ShutdownHandlers() 141 } 142 143 func (n *NodeUntainter) reconcileNode(key types.NamespacedName) error { 144 log.Debugf("reconciling node %s", key.Name) 145 node := n.nodesClient.Get(key.Name, key.Namespace) 146 if node == nil { 147 return nil 148 } 149 150 err := removeReadinessTaint(n.nodesClient, node) 151 if err != nil { 152 log.Errorf("failed to remove readiness taint from node %v: %v", node.Name, err) 153 } 154 return err 155 } 156 157 func removeReadinessTaint(nodesClient kclient.Client[*v1.Node], node *v1.Node) error { 158 updatedTaint := deleteTaint(node.Spec.Taints) 159 if len(updatedTaint) == len(node.Spec.Taints) { 160 // nothing to remove.. 161 return nil 162 } 163 164 log.Debugf("removing readiness taint from node %v", node.Name) 165 atomicTaintRemove := []jsonpatch.Operation{ 166 // make sure taints hadn't chained since we last read the node 167 { 168 Operation: "test", 169 Path: "/spec/taints", 170 Value: node.Spec.Taints, 171 }, 172 // remove the taint 173 { 174 Operation: "replace", 175 Path: "/spec/taints", 176 Value: updatedTaint, 177 }, 178 } 179 patch, err := json.Marshal(atomicTaintRemove) 180 if err != nil { 181 return fmt.Errorf("failed to marshal patch: %v", err) 182 } 183 184 _, err = nodesClient.Patch(node.Name, "", types.JSONPatchType, patch) 185 if err != nil { 186 return fmt.Errorf("failed to update node %v after adding taint: %v", node.Name, err) 187 } 188 log.Debugf("removed readiness taint from node %v", node.Name) 189 return nil 190 } 191 192 // deleteTaint removes all the taints that have the same key and effect to given taintToDelete. 193 func deleteTaint(taints []v1.Taint) []v1.Taint { 194 newTaints := []v1.Taint{} 195 for i := range taints { 196 if taints[i].Key == TaintName { 197 continue 198 } 199 newTaints = append(newTaints, taints[i]) 200 } 201 return newTaints 202 } 203 204 func hasTaint(n *v1.Node) bool { 205 for _, taint := range n.Spec.Taints { 206 if taint.Key == TaintName { 207 return true 208 } 209 } 210 return false 211 } 212 213 // IsPodReady is copied from kubernetes/pkg/api/v1/pod/utils.go 214 func IsPodReady(pod *v1.Pod) bool { 215 return IsPodReadyConditionTrue(pod.Status) 216 } 217 218 // IsPodReadyConditionTrue returns true if a pod is ready; false otherwise. 219 func IsPodReadyConditionTrue(status v1.PodStatus) bool { 220 condition := GetPodReadyCondition(status) 221 return condition != nil && condition.Status == v1.ConditionTrue 222 } 223 224 func GetPodReadyCondition(status v1.PodStatus) *v1.PodCondition { 225 _, condition := GetPodCondition(&status, v1.PodReady) 226 return condition 227 } 228 229 func GetPodCondition(status *v1.PodStatus, conditionType v1.PodConditionType) (int, *v1.PodCondition) { 230 if status == nil { 231 return -1, nil 232 } 233 return GetPodConditionFromList(status.Conditions, conditionType) 234 } 235 236 // GetPodConditionFromList extracts the provided condition from the given list of condition and 237 // returns the index of the condition and the condition. Returns -1 and nil if the condition is not present. 238 func GetPodConditionFromList(conditions []v1.PodCondition, conditionType v1.PodConditionType) (int, *v1.PodCondition) { 239 if conditions == nil { 240 return -1, nil 241 } 242 for i := range conditions { 243 if conditions[i].Type == conditionType { 244 return i, &conditions[i] 245 } 246 } 247 return -1, nil 248 }