mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 13:50:01 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			276 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			276 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2017 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.
 | |
| */
 | |
| 
 | |
| // TODO: This file can potentially be moved to a common place used by both e2e and integration tests.
 | |
| 
 | |
| package framework
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"net/http/httptest"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"k8s.io/api/core/v1"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/fields"
 | |
| 	"k8s.io/apimachinery/pkg/util/wait"
 | |
| 	clientset "k8s.io/client-go/kubernetes"
 | |
| 	"k8s.io/klog/v2"
 | |
| 	v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
 | |
| 	nodectlr "k8s.io/kubernetes/pkg/controller/nodelifecycle"
 | |
| 	testutils "k8s.io/kubernetes/test/utils"
 | |
| )
 | |
| 
 | |
| 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
 | |
| )
 | |
| 
 | |
| // CreateTestingNamespace creates a namespace for testing.
 | |
| func CreateTestingNamespace(baseName string, apiserver *httptest.Server, t *testing.T) *v1.Namespace {
 | |
| 	// TODO: Create a namespace with a given basename.
 | |
| 	// Currently we neither create the namespace nor delete all of its contents at the end.
 | |
| 	// But as long as tests are not using the same namespaces, this should work fine.
 | |
| 	return &v1.Namespace{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			// TODO: Once we start creating namespaces, switch to GenerateName.
 | |
| 			Name: baseName,
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // DeleteTestingNamespace is currently a no-op function.
 | |
| func DeleteTestingNamespace(ns *v1.Namespace, apiserver *httptest.Server, t *testing.T) {
 | |
| 	// TODO: Remove all resources from a given namespace once we implement CreateTestingNamespace.
 | |
| }
 | |
| 
 | |
| // 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(c clientset.Interface) (nodes *v1.NodeList, err error) {
 | |
| 	nodes, err = checkWaitListSchedulableNodes(c)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("listing schedulable nodes error: %s", 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
 | |
| }
 | |
| 
 | |
| // checkWaitListSchedulableNodes is a wrapper around listing nodes supporting retries.
 | |
| func checkWaitListSchedulableNodes(c clientset.Interface) (*v1.NodeList, error) {
 | |
| 	nodes, err := waitListSchedulableNodes(c)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error: %s. Non-retryable failure or timed out while listing nodes for integration test cluster", err)
 | |
| 	}
 | |
| 	return nodes, nil
 | |
| }
 | |
| 
 | |
| // waitListSchedulableNodes is a wrapper around listing nodes supporting retries.
 | |
| func waitListSchedulableNodes(c clientset.Interface) (*v1.NodeList, error) {
 | |
| 	var nodes *v1.NodeList
 | |
| 	var err error
 | |
| 	if wait.PollImmediate(poll, singleCallTimeout, func() (bool, error) {
 | |
| 		nodes, err = c.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{FieldSelector: fields.Set{
 | |
| 			"spec.unschedulable": "false",
 | |
| 		}.AsSelector().String()})
 | |
| 		if err != nil {
 | |
| 			if testutils.IsRetryableAPIError(err) {
 | |
| 				return false, nil
 | |
| 			}
 | |
| 			return false, err
 | |
| 		}
 | |
| 		return true, nil
 | |
| 	}) != nil {
 | |
| 		return nodes, err
 | |
| 	}
 | |
| 	return nodes, nil
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| 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(nodectlr.UnreachableTaintTemplate) || taint.MatchTaint(nodectlr.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 {
 | |
| 						klog.Infof(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 {
 | |
| 					klog.Infof("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 {
 | |
| 				klog.Infof("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 {
 | |
| 		klog.Infof("Couldn't find condition %v on node %v", conditionType, node.Name)
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // 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 {
 | |
| 	nonblockingTaints := ""
 | |
| 	fakePod := &v1.Pod{
 | |
| 		TypeMeta: metav1.TypeMeta{
 | |
| 			Kind:       "Pod",
 | |
| 			APIVersion: "v1",
 | |
| 		},
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      "fake-not-scheduled",
 | |
| 			Namespace: "fake-not-scheduled",
 | |
| 		},
 | |
| 		Spec: v1.PodSpec{
 | |
| 			Containers: []v1.Container{
 | |
| 				{
 | |
| 					Name:  "fake-not-scheduled",
 | |
| 					Image: "fake-not-scheduled",
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	// 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 v1helper.TolerationsTolerateTaintsWithFilter(fakePod.Spec.Tolerations, n.Spec.Taints, func(t *v1.Taint) bool {
 | |
| 		return t.Effect == v1.TaintEffectNoExecute || t.Effect == v1.TaintEffectNoSchedule
 | |
| 	})
 | |
| }
 |