| /* |
| Copyright 2019 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package node |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "net" |
| "strings" |
| "time" |
| |
| "github.com/onsi/ginkgo/v2" |
| "github.com/onsi/gomega" |
| |
| v1 "k8s.io/api/core/v1" |
| "k8s.io/apimachinery/pkg/api/resource" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/conversion" |
| "k8s.io/apimachinery/pkg/fields" |
| "k8s.io/apimachinery/pkg/labels" |
| "k8s.io/apimachinery/pkg/types" |
| "k8s.io/apimachinery/pkg/util/rand" |
| "k8s.io/apimachinery/pkg/util/sets" |
| "k8s.io/apimachinery/pkg/util/strategicpatch" |
| "k8s.io/apimachinery/pkg/util/wait" |
| clientset "k8s.io/client-go/kubernetes" |
| clientretry "k8s.io/client-go/util/retry" |
| "k8s.io/kubernetes/test/e2e/framework" |
| netutil "k8s.io/utils/net" |
| ) |
| |
| const ( |
| // poll is how often to Poll pods, nodes and claims. |
| poll = 2 * time.Second |
| |
| // singleCallTimeout is how long to try single API calls (like 'get' or 'list'). Used to prevent |
| // transient failures from failing tests. |
| singleCallTimeout = 5 * time.Minute |
| |
| // ssh port |
| sshPort = "22" |
| ) |
| |
| var ( |
| // unreachableTaintTemplate is the taint for when a node becomes unreachable. |
| // Copied from pkg/controller/nodelifecycle to avoid pulling extra dependencies |
| unreachableTaintTemplate = &v1.Taint{ |
| Key: v1.TaintNodeUnreachable, |
| Effect: v1.TaintEffectNoExecute, |
| } |
| |
| // notReadyTaintTemplate is the taint for when a node is not ready for executing pods. |
| // Copied from pkg/controller/nodelifecycle to avoid pulling extra dependencies |
| notReadyTaintTemplate = &v1.Taint{ |
| Key: v1.TaintNodeNotReady, |
| Effect: v1.TaintEffectNoExecute, |
| } |
| |
| // updateTaintBackOff contains the maximum retries and the wait interval between two retries. |
| updateTaintBackOff = wait.Backoff{ |
| Steps: 5, |
| Duration: 100 * time.Millisecond, |
| Jitter: 1.0, |
| } |
| ) |
| |
| // PodNode is a pod-node pair indicating which node a given pod is running on |
| type PodNode struct { |
| // Pod represents pod name |
| Pod string |
| // Node represents node name |
| Node string |
| } |
| |
| // FirstAddress returns the first address of the given type of each node. |
| func FirstAddress(nodelist *v1.NodeList, addrType v1.NodeAddressType) string { |
| for _, n := range nodelist.Items { |
| for _, addr := range n.Status.Addresses { |
| if addr.Type == addrType && addr.Address != "" { |
| return addr.Address |
| } |
| } |
| } |
| return "" |
| } |
| |
| func isNodeConditionSetAsExpected(node *v1.Node, conditionType v1.NodeConditionType, wantTrue, silent bool) bool { |
| // Check the node readiness condition (logging all). |
| for _, cond := range node.Status.Conditions { |
| // Ensure that the condition type and the status matches as desired. |
| if cond.Type == conditionType { |
| // For NodeReady condition we need to check Taints as well |
| if cond.Type == v1.NodeReady { |
| hasNodeControllerTaints := false |
| // For NodeReady we need to check if Taints are gone as well |
| taints := node.Spec.Taints |
| for _, taint := range taints { |
| if taint.MatchTaint(unreachableTaintTemplate) || taint.MatchTaint(notReadyTaintTemplate) { |
| hasNodeControllerTaints = true |
| break |
| } |
| } |
| if wantTrue { |
| if (cond.Status == v1.ConditionTrue) && !hasNodeControllerTaints { |
| return true |
| } |
| msg := "" |
| if !hasNodeControllerTaints { |
| msg = fmt.Sprintf("Condition %s of node %s is %v instead of %t. Reason: %v, message: %v", |
| conditionType, node.Name, cond.Status == v1.ConditionTrue, wantTrue, cond.Reason, cond.Message) |
| } else { |
| msg = fmt.Sprintf("Condition %s of node %s is %v, but Node is tainted by NodeController with %v. Failure", |
| conditionType, node.Name, cond.Status == v1.ConditionTrue, taints) |
| } |
| if !silent { |
| framework.Logf(msg) |
| } |
| return false |
| } |
| // TODO: check if the Node is tainted once we enable NC notReady/unreachable taints by default |
| if cond.Status != v1.ConditionTrue { |
| return true |
| } |
| if !silent { |
| framework.Logf("Condition %s of node %s is %v instead of %t. Reason: %v, message: %v", |
| conditionType, node.Name, cond.Status == v1.ConditionTrue, wantTrue, cond.Reason, cond.Message) |
| } |
| return false |
| } |
| if (wantTrue && (cond.Status == v1.ConditionTrue)) || (!wantTrue && (cond.Status != v1.ConditionTrue)) { |
| return true |
| } |
| if !silent { |
| framework.Logf("Condition %s of node %s is %v instead of %t. Reason: %v, message: %v", |
| conditionType, node.Name, cond.Status == v1.ConditionTrue, wantTrue, cond.Reason, cond.Message) |
| } |
| return false |
| } |
| |
| } |
| if !silent { |
| framework.Logf("Couldn't find condition %v on node %v", conditionType, node.Name) |
| } |
| return false |
| } |
| |
| // IsConditionSetAsExpected returns a wantTrue value if the node has a match to the conditionType, otherwise returns an opposite value of the wantTrue with detailed logging. |
| func IsConditionSetAsExpected(node *v1.Node, conditionType v1.NodeConditionType, wantTrue bool) bool { |
| return isNodeConditionSetAsExpected(node, conditionType, wantTrue, false) |
| } |
| |
| // IsConditionSetAsExpectedSilent returns a wantTrue value if the node has a match to the conditionType, otherwise returns an opposite value of the wantTrue. |
| func IsConditionSetAsExpectedSilent(node *v1.Node, conditionType v1.NodeConditionType, wantTrue bool) bool { |
| return isNodeConditionSetAsExpected(node, conditionType, wantTrue, true) |
| } |
| |
| // isConditionUnset returns true if conditions of the given node do not have a match to the given conditionType, otherwise false. |
| func isConditionUnset(node *v1.Node, conditionType v1.NodeConditionType) bool { |
| for _, cond := range node.Status.Conditions { |
| if cond.Type == conditionType { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // Filter filters nodes in NodeList in place, removing nodes that do not |
| // satisfy the given condition |
| func Filter(nodeList *v1.NodeList, fn func(node v1.Node) bool) { |
| var l []v1.Node |
| |
| for _, node := range nodeList.Items { |
| if fn(node) { |
| l = append(l, node) |
| } |
| } |
| nodeList.Items = l |
| } |
| |
| // TotalRegistered returns number of schedulable Nodes. |
| func TotalRegistered(ctx context.Context, c clientset.Interface) (int, error) { |
| nodes, err := waitListSchedulableNodes(ctx, c) |
| if err != nil { |
| framework.Logf("Failed to list nodes: %v", err) |
| return 0, err |
| } |
| return len(nodes.Items), nil |
| } |
| |
| // TotalReady returns number of ready schedulable Nodes. |
| func TotalReady(ctx context.Context, c clientset.Interface) (int, error) { |
| nodes, err := waitListSchedulableNodes(ctx, c) |
| if err != nil { |
| framework.Logf("Failed to list nodes: %v", err) |
| return 0, err |
| } |
| |
| // Filter out not-ready nodes. |
| Filter(nodes, func(node v1.Node) bool { |
| return IsConditionSetAsExpected(&node, v1.NodeReady, true) |
| }) |
| return len(nodes.Items), nil |
| } |
| |
| // GetSSHExternalIP returns node external IP concatenated with port 22 for ssh |
| // e.g. 1.2.3.4:22 |
| func GetSSHExternalIP(node *v1.Node) (string, error) { |
| framework.Logf("Getting external IP address for %s", node.Name) |
| |
| for _, a := range node.Status.Addresses { |
| if a.Type == v1.NodeExternalIP && a.Address != "" { |
| return net.JoinHostPort(a.Address, sshPort), nil |
| } |
| } |
| return "", fmt.Errorf("Couldn't get the external IP of host %s with addresses %v", node.Name, node.Status.Addresses) |
| } |
| |
| // GetSSHInternalIP returns node internal IP concatenated with port 22 for ssh |
| func GetSSHInternalIP(node *v1.Node) (string, error) { |
| for _, address := range node.Status.Addresses { |
| if address.Type == v1.NodeInternalIP && address.Address != "" { |
| return net.JoinHostPort(address.Address, sshPort), nil |
| } |
| } |
| |
| return "", fmt.Errorf("Couldn't get the internal IP of host %s with addresses %v", node.Name, node.Status.Addresses) |
| } |
| |
| // FirstAddressByTypeAndFamily returns the first address that matches the given type and family of the list of nodes |
| func FirstAddressByTypeAndFamily(nodelist *v1.NodeList, addrType v1.NodeAddressType, family v1.IPFamily) string { |
| for _, n := range nodelist.Items { |
| addresses := GetAddressesByTypeAndFamily(&n, addrType, family) |
| if len(addresses) > 0 { |
| return addresses[0] |
| } |
| } |
| return "" |
| } |
| |
| // GetAddressesByTypeAndFamily returns a list of addresses of the given addressType for the given node |
| // and filtered by IPFamily |
| func GetAddressesByTypeAndFamily(node *v1.Node, addressType v1.NodeAddressType, family v1.IPFamily) (ips []string) { |
| for _, nodeAddress := range node.Status.Addresses { |
| if nodeAddress.Type != addressType { |
| continue |
| } |
| if nodeAddress.Address == "" { |
| continue |
| } |
| if family == v1.IPv6Protocol && netutil.IsIPv6String(nodeAddress.Address) { |
| ips = append(ips, nodeAddress.Address) |
| } |
| if family == v1.IPv4Protocol && !netutil.IsIPv6String(nodeAddress.Address) { |
| ips = append(ips, nodeAddress.Address) |
| } |
| } |
| return |
| } |
| |
| // GetAddresses returns a list of addresses of the given addressType for the given node |
| func GetAddresses(node *v1.Node, addressType v1.NodeAddressType) (ips []string) { |
| for j := range node.Status.Addresses { |
| nodeAddress := &node.Status.Addresses[j] |
| if nodeAddress.Type == addressType && nodeAddress.Address != "" { |
| ips = append(ips, nodeAddress.Address) |
| } |
| } |
| return |
| } |
| |
| // CollectAddresses returns a list of addresses of the given addressType for the given list of nodes |
| func CollectAddresses(nodes *v1.NodeList, addressType v1.NodeAddressType) []string { |
| ips := []string{} |
| for i := range nodes.Items { |
| ips = append(ips, GetAddresses(&nodes.Items[i], addressType)...) |
| } |
| return ips |
| } |
| |
| // PickIP picks one public node IP |
| func PickIP(ctx context.Context, c clientset.Interface) (string, error) { |
| publicIps, err := GetPublicIps(ctx, c) |
| if err != nil { |
| return "", fmt.Errorf("get node public IPs error: %w", err) |
| } |
| if len(publicIps) == 0 { |
| return "", fmt.Errorf("got unexpected number (%d) of public IPs", len(publicIps)) |
| } |
| ip := publicIps[0] |
| return ip, nil |
| } |
| |
| // GetPublicIps returns a public IP list of nodes. |
| func GetPublicIps(ctx context.Context, c clientset.Interface) ([]string, error) { |
| nodes, err := GetReadySchedulableNodes(ctx, c) |
| if err != nil { |
| return nil, fmt.Errorf("get schedulable and ready nodes error: %w", err) |
| } |
| ips := CollectAddresses(nodes, v1.NodeExternalIP) |
| if len(ips) == 0 { |
| // If ExternalIP isn't set, assume the test programs can reach the InternalIP |
| ips = CollectAddresses(nodes, v1.NodeInternalIP) |
| } |
| return ips, nil |
| } |
| |
| // GetReadySchedulableNodes addresses the common use case of getting nodes you can do work on. |
| // 1) Needs to be schedulable. |
| // 2) Needs to be ready. |
| // If EITHER 1 or 2 is not true, most tests will want to ignore the node entirely. |
| // If there are no nodes that are both ready and schedulable, this will return an error. |
| func GetReadySchedulableNodes(ctx context.Context, c clientset.Interface) (nodes *v1.NodeList, err error) { |
| nodes, err = checkWaitListSchedulableNodes(ctx, c) |
| if err != nil { |
| return nil, fmt.Errorf("listing schedulable nodes error: %w", err) |
| } |
| Filter(nodes, func(node v1.Node) bool { |
| return IsNodeSchedulable(&node) && isNodeUntainted(&node) |
| }) |
| if len(nodes.Items) == 0 { |
| return nil, fmt.Errorf("there are currently no ready, schedulable nodes in the cluster") |
| } |
| return nodes, nil |
| } |
| |
| // GetBoundedReadySchedulableNodes is like GetReadySchedulableNodes except that it returns |
| // at most maxNodes nodes. Use this to keep your test case from blowing up when run on a |
| // large cluster. |
| func GetBoundedReadySchedulableNodes(ctx context.Context, c clientset.Interface, maxNodes int) (nodes *v1.NodeList, err error) { |
| nodes, err = GetReadySchedulableNodes(ctx, c) |
| if err != nil { |
| return nil, err |
| } |
| if len(nodes.Items) > maxNodes { |
| shuffled := make([]v1.Node, maxNodes) |
| perm := rand.Perm(len(nodes.Items)) |
| for i, j := range perm { |
| if j < len(shuffled) { |
| shuffled[j] = nodes.Items[i] |
| } |
| } |
| nodes.Items = shuffled |
| } |
| return nodes, nil |
| } |
| |
| // GetRandomReadySchedulableNode gets a single randomly-selected node which is available for |
| // running pods on. If there are no available nodes it will return an error. |
| func GetRandomReadySchedulableNode(ctx context.Context, c clientset.Interface) (*v1.Node, error) { |
| nodes, err := GetReadySchedulableNodes(ctx, c) |
| if err != nil { |
| return nil, err |
| } |
| return &nodes.Items[rand.Intn(len(nodes.Items))], nil |
| } |
| |
| // GetReadyNodesIncludingTainted returns all ready nodes, even those which are tainted. |
| // There are cases when we care about tainted nodes |
| // E.g. in tests related to nodes with gpu we care about nodes despite |
| // presence of nvidia.com/gpu=present:NoSchedule taint |
| func GetReadyNodesIncludingTainted(ctx context.Context, c clientset.Interface) (nodes *v1.NodeList, err error) { |
| nodes, err = checkWaitListSchedulableNodes(ctx, c) |
| if err != nil { |
| return nil, fmt.Errorf("listing schedulable nodes error: %w", err) |
| } |
| Filter(nodes, func(node v1.Node) bool { |
| return IsNodeSchedulable(&node) |
| }) |
| return nodes, nil |
| } |
| |
| // isNodeUntainted tests whether a fake pod can be scheduled on "node", given its current taints. |
| // TODO: need to discuss wether to return bool and error type |
| func isNodeUntainted(node *v1.Node) bool { |
| return isNodeUntaintedWithNonblocking(node, "") |
| } |
| |
| // isNodeUntaintedWithNonblocking tests whether a fake pod can be scheduled on "node" |
| // but allows for taints in the list of non-blocking taints. |
| func isNodeUntaintedWithNonblocking(node *v1.Node, nonblockingTaints string) bool { |
| // Simple lookup for nonblocking taints based on comma-delimited list. |
| nonblockingTaintsMap := map[string]struct{}{} |
| for _, t := range strings.Split(nonblockingTaints, ",") { |
| if strings.TrimSpace(t) != "" { |
| nonblockingTaintsMap[strings.TrimSpace(t)] = struct{}{} |
| } |
| } |
| |
| n := node |
| if len(nonblockingTaintsMap) > 0 { |
| nodeCopy := node.DeepCopy() |
| nodeCopy.Spec.Taints = []v1.Taint{} |
| for _, v := range node.Spec.Taints { |
| if _, isNonblockingTaint := nonblockingTaintsMap[v.Key]; !isNonblockingTaint { |
| nodeCopy.Spec.Taints = append(nodeCopy.Spec.Taints, v) |
| } |
| } |
| n = nodeCopy |
| } |
| |
| return toleratesTaintsWithNoScheduleNoExecuteEffects(n.Spec.Taints, nil) |
| } |
| |
| func toleratesTaintsWithNoScheduleNoExecuteEffects(taints []v1.Taint, tolerations []v1.Toleration) bool { |
| filteredTaints := []v1.Taint{} |
| for _, taint := range taints { |
| if taint.Effect == v1.TaintEffectNoExecute || taint.Effect == v1.TaintEffectNoSchedule { |
| filteredTaints = append(filteredTaints, taint) |
| } |
| } |
| |
| toleratesTaint := func(taint v1.Taint) bool { |
| for _, toleration := range tolerations { |
| if toleration.ToleratesTaint(&taint) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| for _, taint := range filteredTaints { |
| if !toleratesTaint(taint) { |
| return false |
| } |
| } |
| |
| return true |
| } |
| |
| // IsNodeSchedulable returns true if: |
| // 1) doesn't have "unschedulable" field set |
| // 2) it also returns true from IsNodeReady |
| func IsNodeSchedulable(node *v1.Node) bool { |
| if node == nil { |
| return false |
| } |
| return !node.Spec.Unschedulable && IsNodeReady(node) |
| } |
| |
| // IsNodeReady returns true if: |
| // 1) it's Ready condition is set to true |
| // 2) doesn't have NetworkUnavailable condition set to true |
| func IsNodeReady(node *v1.Node) bool { |
| nodeReady := IsConditionSetAsExpected(node, v1.NodeReady, true) |
| networkReady := isConditionUnset(node, v1.NodeNetworkUnavailable) || |
| IsConditionSetAsExpectedSilent(node, v1.NodeNetworkUnavailable, false) |
| return nodeReady && networkReady |
| } |
| |
| // isNodeSchedulableWithoutTaints returns true if: |
| // 1) doesn't have "unschedulable" field set |
| // 2) it also returns true from IsNodeReady |
| // 3) it also returns true from isNodeUntainted |
| func isNodeSchedulableWithoutTaints(node *v1.Node) bool { |
| return IsNodeSchedulable(node) && isNodeUntainted(node) |
| } |
| |
| // hasNonblockingTaint returns true if the node contains at least |
| // one taint with a key matching the regexp. |
| func hasNonblockingTaint(node *v1.Node, nonblockingTaints string) bool { |
| if node == nil { |
| return false |
| } |
| |
| // Simple lookup for nonblocking taints based on comma-delimited list. |
| nonblockingTaintsMap := map[string]struct{}{} |
| for _, t := range strings.Split(nonblockingTaints, ",") { |
| if strings.TrimSpace(t) != "" { |
| nonblockingTaintsMap[strings.TrimSpace(t)] = struct{}{} |
| } |
| } |
| |
| for _, taint := range node.Spec.Taints { |
| if _, hasNonblockingTaint := nonblockingTaintsMap[taint.Key]; hasNonblockingTaint { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| // PodNodePairs return podNode pairs for all pods in a namespace |
| func PodNodePairs(ctx context.Context, c clientset.Interface, ns string) ([]PodNode, error) { |
| var result []PodNode |
| |
| podList, err := c.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{}) |
| if err != nil { |
| return result, err |
| } |
| |
| for _, pod := range podList.Items { |
| result = append(result, PodNode{ |
| Pod: pod.Name, |
| Node: pod.Spec.NodeName, |
| }) |
| } |
| |
| return result, nil |
| } |
| |
| // GetClusterZones returns the values of zone label collected from all nodes. |
| func GetClusterZones(ctx context.Context, c clientset.Interface) (sets.String, error) { |
| nodes, err := c.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) |
| if err != nil { |
| return nil, fmt.Errorf("Error getting nodes while attempting to list cluster zones: %w", err) |
| } |
| |
| // collect values of zone label from all nodes |
| zones := sets.NewString() |
| for _, node := range nodes.Items { |
| if zone, found := node.Labels[v1.LabelFailureDomainBetaZone]; found { |
| zones.Insert(zone) |
| } |
| |
| if zone, found := node.Labels[v1.LabelTopologyZone]; found { |
| zones.Insert(zone) |
| } |
| } |
| return zones, nil |
| } |
| |
| // GetSchedulableClusterZones returns the values of zone label collected from all nodes which are schedulable. |
| func GetSchedulableClusterZones(ctx context.Context, c clientset.Interface) (sets.Set[string], error) { |
| // GetReadySchedulableNodes already filters our tainted and unschedulable nodes. |
| nodes, err := GetReadySchedulableNodes(ctx, c) |
| if err != nil { |
| return nil, fmt.Errorf("error getting nodes while attempting to list cluster zones: %w", err) |
| } |
| |
| // collect values of zone label from all nodes |
| zones := sets.New[string]() |
| for _, node := range nodes.Items { |
| if zone, found := node.Labels[v1.LabelFailureDomainBetaZone]; found { |
| zones.Insert(zone) |
| } |
| |
| if zone, found := node.Labels[v1.LabelTopologyZone]; found { |
| zones.Insert(zone) |
| } |
| } |
| return zones, nil |
| } |
| |
| // CreatePodsPerNodeForSimpleApp creates pods w/ labels. Useful for tests which make a bunch of pods w/o any networking. |
| func CreatePodsPerNodeForSimpleApp(ctx context.Context, c clientset.Interface, namespace, appName string, podSpec func(n v1.Node) v1.PodSpec, maxCount int) map[string]string { |
| nodes, err := GetBoundedReadySchedulableNodes(ctx, c, maxCount) |
| // TODO use wrapper methods in expect.go after removing core e2e dependency on node |
| gomega.ExpectWithOffset(2, err).NotTo(gomega.HaveOccurred()) |
| podLabels := map[string]string{ |
| "app": appName + "-pod", |
| } |
| for i, node := range nodes.Items { |
| framework.Logf("%v/%v : Creating container with label app=%v-pod", i, maxCount, appName) |
| _, err := c.CoreV1().Pods(namespace).Create(ctx, &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: fmt.Sprintf(appName+"-pod-%v", i), |
| Labels: podLabels, |
| }, |
| Spec: podSpec(node), |
| }, metav1.CreateOptions{}) |
| // TODO use wrapper methods in expect.go after removing core e2e dependency on node |
| gomega.ExpectWithOffset(2, err).NotTo(gomega.HaveOccurred()) |
| } |
| return podLabels |
| } |
| |
| // RemoveTaintsOffNode removes a list of taints from the given node |
| // It is simply a helper wrapper for RemoveTaintOffNode |
| func RemoveTaintsOffNode(ctx context.Context, c clientset.Interface, nodeName string, taints []v1.Taint) { |
| for _, taint := range taints { |
| RemoveTaintOffNode(ctx, c, nodeName, taint) |
| } |
| } |
| |
| // RemoveTaintOffNode removes the given taint from the given node. |
| func RemoveTaintOffNode(ctx context.Context, c clientset.Interface, nodeName string, taint v1.Taint) { |
| err := removeNodeTaint(ctx, c, nodeName, nil, &taint) |
| |
| // TODO use wrapper methods in expect.go after removing core e2e dependency on node |
| gomega.ExpectWithOffset(2, err).NotTo(gomega.HaveOccurred()) |
| verifyThatTaintIsGone(ctx, c, nodeName, &taint) |
| } |
| |
| // AddOrUpdateTaintOnNode adds the given taint to the given node or updates taint. |
| func AddOrUpdateTaintOnNode(ctx context.Context, c clientset.Interface, nodeName string, taint v1.Taint) { |
| // TODO use wrapper methods in expect.go after removing the dependency on this |
| // package from the core e2e framework. |
| err := addOrUpdateTaintOnNode(ctx, c, nodeName, &taint) |
| gomega.ExpectWithOffset(2, err).NotTo(gomega.HaveOccurred()) |
| } |
| |
| // addOrUpdateTaintOnNode add taints to the node. If taint was added into node, it'll issue API calls |
| // to update nodes; otherwise, no API calls. Return error if any. |
| // copied from pkg/controller/controller_utils.go AddOrUpdateTaintOnNode() |
| func addOrUpdateTaintOnNode(ctx context.Context, c clientset.Interface, nodeName string, taints ...*v1.Taint) error { |
| if len(taints) == 0 { |
| return nil |
| } |
| firstTry := true |
| return clientretry.RetryOnConflict(updateTaintBackOff, func() error { |
| var err error |
| var oldNode *v1.Node |
| // First we try getting node from the API server cache, as it's cheaper. If it fails |
| // we get it from etcd to be sure to have fresh data. |
| if firstTry { |
| oldNode, err = c.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{ResourceVersion: "0"}) |
| firstTry = false |
| } else { |
| oldNode, err = c.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) |
| } |
| if err != nil { |
| return err |
| } |
| |
| var newNode *v1.Node |
| oldNodeCopy := oldNode |
| updated := false |
| for _, taint := range taints { |
| curNewNode, ok, err := addOrUpdateTaint(oldNodeCopy, taint) |
| if err != nil { |
| return fmt.Errorf("failed to update taint of node") |
| } |
| updated = updated || ok |
| newNode = curNewNode |
| oldNodeCopy = curNewNode |
| } |
| if !updated { |
| return nil |
| } |
| return patchNodeTaints(ctx, c, nodeName, oldNode, newNode) |
| }) |
| } |
| |
| // addOrUpdateTaint tries to add a taint to annotations list. Returns a new copy of updated Node and true if something was updated |
| // false otherwise. |
| // copied from pkg/util/taints/taints.go AddOrUpdateTaint() |
| func addOrUpdateTaint(node *v1.Node, taint *v1.Taint) (*v1.Node, bool, error) { |
| newNode := node.DeepCopy() |
| nodeTaints := newNode.Spec.Taints |
| |
| var newTaints []v1.Taint |
| updated := false |
| for i := range nodeTaints { |
| if taint.MatchTaint(&nodeTaints[i]) { |
| if semantic.DeepEqual(*taint, nodeTaints[i]) { |
| return newNode, false, nil |
| } |
| newTaints = append(newTaints, *taint) |
| updated = true |
| continue |
| } |
| |
| newTaints = append(newTaints, nodeTaints[i]) |
| } |
| |
| if !updated { |
| newTaints = append(newTaints, *taint) |
| } |
| |
| newNode.Spec.Taints = newTaints |
| return newNode, true, nil |
| } |
| |
| // semantic can do semantic deep equality checks for core objects. |
| // Example: apiequality.Semantic.DeepEqual(aPod, aPodWithNonNilButEmptyMaps) == true |
| // copied from pkg/apis/core/helper/helpers.go Semantic |
| var semantic = conversion.EqualitiesOrDie( |
| func(a, b resource.Quantity) bool { |
| // Ignore formatting, only care that numeric value stayed the same. |
| // TODO: if we decide it's important, it should be safe to start comparing the format. |
| // |
| // Uninitialized quantities are equivalent to 0 quantities. |
| return a.Cmp(b) == 0 |
| }, |
| func(a, b metav1.MicroTime) bool { |
| return a.UTC() == b.UTC() |
| }, |
| func(a, b metav1.Time) bool { |
| return a.UTC() == b.UTC() |
| }, |
| func(a, b labels.Selector) bool { |
| return a.String() == b.String() |
| }, |
| func(a, b fields.Selector) bool { |
| return a.String() == b.String() |
| }, |
| ) |
| |
| // removeNodeTaint is for cleaning up taints temporarily added to node, |
| // won't fail if target taint doesn't exist or has been removed. |
| // If passed a node it'll check if there's anything to be done, if taint is not present it won't issue |
| // any API calls. |
| func removeNodeTaint(ctx context.Context, c clientset.Interface, nodeName string, node *v1.Node, taints ...*v1.Taint) error { |
| if len(taints) == 0 { |
| return nil |
| } |
| // Short circuit for limiting amount of API calls. |
| if node != nil { |
| match := false |
| for _, taint := range taints { |
| if taintExists(node.Spec.Taints, taint) { |
| match = true |
| break |
| } |
| } |
| if !match { |
| return nil |
| } |
| } |
| |
| firstTry := true |
| return clientretry.RetryOnConflict(updateTaintBackOff, func() error { |
| var err error |
| var oldNode *v1.Node |
| // First we try getting node from the API server cache, as it's cheaper. If it fails |
| // we get it from etcd to be sure to have fresh data. |
| if firstTry { |
| oldNode, err = c.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{ResourceVersion: "0"}) |
| firstTry = false |
| } else { |
| oldNode, err = c.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) |
| } |
| if err != nil { |
| return err |
| } |
| |
| var newNode *v1.Node |
| oldNodeCopy := oldNode |
| updated := false |
| for _, taint := range taints { |
| curNewNode, ok, err := removeTaint(oldNodeCopy, taint) |
| if err != nil { |
| return fmt.Errorf("failed to remove taint of node") |
| } |
| updated = updated || ok |
| newNode = curNewNode |
| oldNodeCopy = curNewNode |
| } |
| if !updated { |
| return nil |
| } |
| return patchNodeTaints(ctx, c, nodeName, oldNode, newNode) |
| }) |
| } |
| |
| // patchNodeTaints patches node's taints. |
| func patchNodeTaints(ctx context.Context, c clientset.Interface, nodeName string, oldNode *v1.Node, newNode *v1.Node) error { |
| oldData, err := json.Marshal(oldNode) |
| if err != nil { |
| return fmt.Errorf("failed to marshal old node %#v for node %q: %w", oldNode, nodeName, err) |
| } |
| |
| newTaints := newNode.Spec.Taints |
| newNodeClone := oldNode.DeepCopy() |
| newNodeClone.Spec.Taints = newTaints |
| newData, err := json.Marshal(newNodeClone) |
| if err != nil { |
| return fmt.Errorf("failed to marshal new node %#v for node %q: %w", newNodeClone, nodeName, err) |
| } |
| |
| patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, v1.Node{}) |
| if err != nil { |
| return fmt.Errorf("failed to create patch for node %q: %w", nodeName, err) |
| } |
| |
| _, err = c.CoreV1().Nodes().Patch(ctx, nodeName, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}) |
| return err |
| } |
| |
| // removeTaint tries to remove a taint from annotations list. Returns a new copy of updated Node and true if something was updated |
| // false otherwise. |
| func removeTaint(node *v1.Node, taint *v1.Taint) (*v1.Node, bool, error) { |
| newNode := node.DeepCopy() |
| nodeTaints := newNode.Spec.Taints |
| if len(nodeTaints) == 0 { |
| return newNode, false, nil |
| } |
| |
| if !taintExists(nodeTaints, taint) { |
| return newNode, false, nil |
| } |
| |
| newTaints, _ := deleteTaint(nodeTaints, taint) |
| newNode.Spec.Taints = newTaints |
| return newNode, true, nil |
| } |
| |
| // deleteTaint removes all the taints that have the same key and effect to given taintToDelete. |
| func deleteTaint(taints []v1.Taint, taintToDelete *v1.Taint) ([]v1.Taint, bool) { |
| var newTaints []v1.Taint |
| deleted := false |
| for i := range taints { |
| if taintToDelete.MatchTaint(&taints[i]) { |
| deleted = true |
| continue |
| } |
| newTaints = append(newTaints, taints[i]) |
| } |
| return newTaints, deleted |
| } |
| |
| func verifyThatTaintIsGone(ctx context.Context, c clientset.Interface, nodeName string, taint *v1.Taint) { |
| ginkgo.By("verifying the node doesn't have the taint " + taint.ToString()) |
| nodeUpdated, err := c.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) |
| |
| // TODO use wrapper methods in expect.go after removing core e2e dependency on node |
| gomega.ExpectWithOffset(2, err).NotTo(gomega.HaveOccurred()) |
| if taintExists(nodeUpdated.Spec.Taints, taint) { |
| framework.Failf("Failed removing taint " + taint.ToString() + " of the node " + nodeName) |
| } |
| } |