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  }