k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/measurement/util/runtimeobjects/runtimeobjects.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package runtimeobjects
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strconv"
    23  
    24  	goerrors "github.com/go-errors/errors"
    25  	gocmp "github.com/google/go-cmp/cmp"
    26  	appsv1 "k8s.io/api/apps/v1"
    27  	batch "k8s.io/api/batch/v1"
    28  	corev1 "k8s.io/api/core/v1"
    29  	"k8s.io/apimachinery/pkg/api/equality"
    30  	"k8s.io/apimachinery/pkg/api/meta"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/client-go/dynamic"
    37  	clientset "k8s.io/client-go/kubernetes"
    38  	corev1helpers "k8s.io/component-helpers/scheduling/corev1"
    39  	v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
    40  	dsutil "k8s.io/kubernetes/pkg/controller/daemon/util"
    41  	"k8s.io/perf-tests/clusterloader2/pkg/framework/client"
    42  )
    43  
    44  // ListRuntimeObjectsForKind returns objects of given kind that satisfy given namespace, labelSelector and fieldSelector.
    45  func ListRuntimeObjectsForKind(d dynamic.Interface, gvr schema.GroupVersionResource, kind, namespace, labelSelector, fieldSelector string) ([]runtime.Object, error) {
    46  	var runtimeObjectsList []runtime.Object
    47  	var listFunc func() error
    48  	listOpts := metav1.ListOptions{
    49  		LabelSelector: labelSelector,
    50  		FieldSelector: fieldSelector,
    51  	}
    52  	listFunc = func() error {
    53  		list, err := d.Resource(gvr).List(context.TODO(), listOpts)
    54  		if err != nil {
    55  			return err
    56  		}
    57  		runtimeObjectsList = make([]runtime.Object, len(list.Items))
    58  		for i := range list.Items {
    59  			runtimeObjectsList[i] = &list.Items[i]
    60  		}
    61  		return nil
    62  	}
    63  
    64  	if err := client.RetryWithExponentialBackOff(client.RetryFunction(listFunc)); err != nil {
    65  		return nil, err
    66  	}
    67  	return runtimeObjectsList, nil
    68  }
    69  
    70  // GetResourceVersionFromRuntimeObject returns resource version of given runtime object.
    71  func GetResourceVersionFromRuntimeObject(obj runtime.Object) (uint64, error) {
    72  	accessor, err := meta.Accessor(obj)
    73  	if err != nil {
    74  		return 0, fmt.Errorf("accessor error: %v", err)
    75  	}
    76  	version := accessor.GetResourceVersion()
    77  	if len(version) == 0 {
    78  		return 0, nil
    79  	}
    80  	return strconv.ParseUint(version, 10, 64)
    81  }
    82  
    83  // GetIsPodUpdatedPredicateFromRuntimeObject returns a func(*corev1.Pod) bool predicate
    84  // that can be used to check if given pod represents the desired state of pod.
    85  func GetIsPodUpdatedPredicateFromRuntimeObject(obj runtime.Object) (func(*corev1.Pod) error, error) {
    86  	switch typed := obj.(type) {
    87  	case *unstructured.Unstructured:
    88  		return getIsPodUpdatedPodPredicateFromUnstructured(typed)
    89  	default:
    90  		return nil, goerrors.Errorf("unsupported kind when getting updated pod predicate: %v", obj)
    91  	}
    92  }
    93  
    94  // Auxiliary error type for lazy evaluation of gocmp.Diff which is known to be
    95  // computationally expensive.
    96  type lazySpecDiffError struct {
    97  	templateSpec corev1.PodSpec
    98  	podSpec      corev1.PodSpec
    99  }
   100  
   101  func (lsde *lazySpecDiffError) Error() string {
   102  	return fmt.Sprintf("Not matching templates, diff: %v", gocmp.Diff(lsde.templateSpec, lsde.podSpec))
   103  }
   104  
   105  func getIsPodUpdatedPodPredicateFromUnstructured(obj *unstructured.Unstructured) (func(_ *corev1.Pod) error, error) {
   106  	templateMap, ok, err := unstructured.NestedMap(obj.UnstructuredContent(), "spec", "template")
   107  	if err != nil {
   108  		return nil, goerrors.Errorf("failed to get pod template: %v", err)
   109  	}
   110  	if !ok {
   111  		return nil, goerrors.Errorf("spec.template is not set in object %v", obj.UnstructuredContent())
   112  	}
   113  	template := corev1.PodTemplateSpec{}
   114  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(templateMap, &template); err != nil {
   115  		return nil, goerrors.Errorf("failed to parse spec.teemplate as v1.PodTemplateSpec")
   116  	}
   117  
   118  	return func(pod *corev1.Pod) error {
   119  		if !equality.Semantic.DeepDerivative(template.Spec, pod.Spec) {
   120  			return &lazySpecDiffError{template.Spec, pod.Spec}
   121  		}
   122  		return nil
   123  	}, nil
   124  }
   125  
   126  // GetSpecFromRuntimeObject returns spec of given runtime object.
   127  func GetSpecFromRuntimeObject(obj runtime.Object) (interface{}, error) {
   128  	if obj == nil {
   129  		return nil, nil
   130  	}
   131  	switch typed := obj.(type) {
   132  	case *unstructured.Unstructured:
   133  		return getSpecFromUnstrutured(typed)
   134  	case *corev1.ReplicationController:
   135  		return typed.Spec, nil
   136  	case *appsv1.ReplicaSet:
   137  		return typed.Spec, nil
   138  	case *appsv1.Deployment:
   139  		return typed.Spec, nil
   140  	case *appsv1.StatefulSet:
   141  		return typed.Spec, nil
   142  	case *appsv1.DaemonSet:
   143  		return typed.Spec, nil
   144  	case *batch.Job:
   145  		return typed.Spec, nil
   146  	default:
   147  		return nil, fmt.Errorf("unsupported kind when getting spec: %v", obj)
   148  	}
   149  }
   150  
   151  // Note: This function assumes each controller has field Spec.
   152  func getSpecFromUnstrutured(obj *unstructured.Unstructured) (map[string]interface{}, error) {
   153  	spec, ok, err := unstructured.NestedMap(obj.UnstructuredContent(), "spec")
   154  	if err != nil {
   155  		return nil, fmt.Errorf("try to acquire spec failed, %v", err)
   156  	}
   157  	if !ok {
   158  		return nil, fmt.Errorf("try to acquire spec failed, no field spec for obj %s", obj.GetName())
   159  	}
   160  	return spec, nil
   161  }
   162  
   163  // GetReplicasFromRuntimeObject returns replicas number from given runtime object.
   164  func GetReplicasFromRuntimeObject(c clientset.Interface, obj runtime.Object) (ReplicasWatcher, error) {
   165  	if obj == nil {
   166  		return &ConstReplicas{0}, nil
   167  	}
   168  	switch typed := obj.(type) {
   169  	case *unstructured.Unstructured:
   170  		return getReplicasFromUnstrutured(c, typed)
   171  	case *corev1.ReplicationController:
   172  		if typed.Spec.Replicas != nil {
   173  			return &ConstReplicas{int(*typed.Spec.Replicas)}, nil
   174  		}
   175  		return &ConstReplicas{0}, nil
   176  	case *appsv1.ReplicaSet:
   177  		if typed.Spec.Replicas != nil {
   178  			return &ConstReplicas{int(*typed.Spec.Replicas)}, nil
   179  		}
   180  		return &ConstReplicas{0}, nil
   181  	case *appsv1.Deployment:
   182  		if typed.Spec.Replicas != nil {
   183  			return &ConstReplicas{int(*typed.Spec.Replicas)}, nil
   184  		}
   185  		return &ConstReplicas{0}, nil
   186  	case *appsv1.StatefulSet:
   187  		if typed.Spec.Replicas != nil {
   188  			return &ConstReplicas{int(*typed.Spec.Replicas)}, nil
   189  		}
   190  		return &ConstReplicas{0}, nil
   191  	case *appsv1.DaemonSet:
   192  		return getDaemonSetNumSchedulableNodes(c, &typed.Spec.Template.Spec)
   193  	case *batch.Job:
   194  		if typed.Spec.Parallelism != nil {
   195  			return &ConstReplicas{int(*typed.Spec.Parallelism)}, nil
   196  		}
   197  		return &ConstReplicas{0}, nil
   198  	default:
   199  		return nil, fmt.Errorf("unsupported kind when getting number of replicas: %v", obj)
   200  	}
   201  }
   202  
   203  // getDaemonSetNumSchedulableNodes returns the number of schedulable nodes matching both nodeSelector and NodeAffinity.
   204  func getDaemonSetNumSchedulableNodes(c clientset.Interface, podSpec *corev1.PodSpec) (ReplicasWatcher, error) {
   205  	selector, err := metav1.LabelSelectorAsSelector(metav1.SetAsLabelSelector(podSpec.NodeSelector))
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	return NewNodeCounter(c, selector, podSpec.Affinity, podSpec.Tolerations), nil
   210  }
   211  
   212  // Note: This function assumes each controller has field Spec.Replicas, except DaemonSets and Job.
   213  func getReplicasFromUnstrutured(c clientset.Interface, obj *unstructured.Unstructured) (ReplicasWatcher, error) {
   214  	spec, err := getSpecFromUnstrutured(obj)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	return tryAcquireReplicasFromUnstructuredSpec(c, spec, obj.GetKind())
   219  }
   220  
   221  func tryAcquireReplicasFromUnstructuredSpec(c clientset.Interface, spec map[string]interface{}, kind string) (ReplicasWatcher, error) {
   222  	switch kind {
   223  	case "DaemonSet":
   224  		parser, err := newDaemonSetPodSpecParser(spec)
   225  		if err != nil {
   226  			return nil, err
   227  		}
   228  		var podSpec corev1.PodSpec
   229  		if err := parser.getDaemonSetNodeSelectorFromUnstructuredSpec(&podSpec); err != nil {
   230  			return nil, err
   231  		}
   232  		if err := parser.getDaemonSetAffinityFromUnstructuredSpec(&podSpec); err != nil {
   233  			return nil, err
   234  		}
   235  		if err := parser.getDaemonSetTolerationsFromUnstructuredSpec(&podSpec); err != nil {
   236  			return nil, err
   237  		}
   238  		return getDaemonSetNumSchedulableNodes(c, &podSpec)
   239  	case "Job":
   240  		replicas, found, err := unstructured.NestedInt64(spec, "parallelism")
   241  		if err != nil {
   242  			return nil, fmt.Errorf("try to acquire job parallelism failed, %v", err)
   243  		}
   244  		if !found {
   245  			return &ConstReplicas{0}, nil
   246  		}
   247  		return &ConstReplicas{int(replicas)}, nil
   248  	default:
   249  		replicas, found, err := unstructured.NestedInt64(spec, "replicas")
   250  		if err != nil {
   251  			return nil, fmt.Errorf("try to acquire replicas failed, %v", err)
   252  		}
   253  		if !found {
   254  			return &ConstReplicas{0}, nil
   255  		}
   256  		return &ConstReplicas{int(replicas)}, nil
   257  	}
   258  }
   259  
   260  type daemonSetPodSpecParser map[string]interface{}
   261  
   262  func newDaemonSetPodSpecParser(spec map[string]interface{}) (daemonSetPodSpecParser, error) {
   263  	template, found, err := unstructured.NestedMap(spec, "template")
   264  	if err != nil || !found {
   265  		return nil, err
   266  	}
   267  	podSpec, found, err := unstructured.NestedMap(template, "spec")
   268  	if err != nil || !found {
   269  		return nil, err
   270  	}
   271  	return podSpec, nil
   272  }
   273  
   274  func (p daemonSetPodSpecParser) getDaemonSetNodeSelectorFromUnstructuredSpec(spec *corev1.PodSpec) error {
   275  	nodeSelector, _, err := unstructured.NestedStringMap(p, "nodeSelector")
   276  	spec.NodeSelector = nodeSelector
   277  	return err
   278  }
   279  
   280  func (p daemonSetPodSpecParser) getDaemonSetAffinityFromUnstructuredSpec(spec *corev1.PodSpec) error {
   281  	unstructuredAffinity, found, err := unstructured.NestedMap(p, "affinity")
   282  	if err != nil || !found {
   283  		return err
   284  	}
   285  	affinity := &corev1.Affinity{}
   286  	err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredAffinity, affinity)
   287  	spec.Affinity = affinity
   288  	return err
   289  }
   290  
   291  func (p daemonSetPodSpecParser) getDaemonSetTolerationsFromUnstructuredSpec(spec *corev1.PodSpec) error {
   292  	dsutil.AddOrUpdateDaemonPodTolerations(spec)
   293  	unstructuredTolerations, found, err := unstructured.NestedSlice(p, "tolerations")
   294  	if err != nil || !found {
   295  		return err
   296  	}
   297  	for _, unstructuredToleration := range unstructuredTolerations {
   298  		var toleration corev1.Toleration
   299  		err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredToleration.(map[string]interface{}), &toleration)
   300  		if err != nil {
   301  			break
   302  		}
   303  		v1helper.AddOrUpdateTolerationInPodSpec(spec, &toleration)
   304  	}
   305  	return err
   306  }
   307  
   308  // IsEqualRuntimeObjectsSpec returns true if given runtime objects have identical specs.
   309  func IsEqualRuntimeObjectsSpec(runtimeObj1, runtimeObj2 runtime.Object) (bool, error) {
   310  	runtimeObj1Spec, err := GetSpecFromRuntimeObject(runtimeObj1)
   311  	if err != nil {
   312  		return false, err
   313  	}
   314  	runtimeObj2Spec, err := GetSpecFromRuntimeObject(runtimeObj2)
   315  	if err != nil {
   316  		return false, err
   317  	}
   318  
   319  	return equality.Semantic.DeepEqual(runtimeObj1Spec, runtimeObj2Spec), nil
   320  }
   321  
   322  // GetNumObjectsMatchingSelector returns number of objects matching the given selector.
   323  func GetNumObjectsMatchingSelector(c dynamic.Interface, namespace string, resource schema.GroupVersionResource, labelSelector labels.Selector) (int, error) {
   324  	var numObjects int
   325  	listFunc := func() error {
   326  		list, err := c.Resource(resource).Namespace(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector.String()})
   327  		if err != nil {
   328  			return err
   329  		}
   330  		numObjects = len(list.Items)
   331  		return nil
   332  	}
   333  	err := client.RetryWithExponentialBackOff(client.RetryFunction(listFunc))
   334  	return numObjects, err
   335  }
   336  
   337  // The pod can only schedule onto nodes that satisfy requirements in NodeAffinity.
   338  func podMatchesNodeAffinity(affinity *corev1.Affinity, node *corev1.Node) (bool, error) {
   339  	// 1. nil NodeSelector matches all nodes (i.e. does not filter out any nodes)
   340  	// 2. nil []NodeSelectorTerm (equivalent to non-nil empty NodeSelector) matches no nodes
   341  	// 3. zero-length non-nil []NodeSelectorTerm matches no nodes also, just for simplicity
   342  	// 4. nil []NodeSelectorRequirement (equivalent to non-nil empty NodeSelectorTerm) matches no nodes
   343  	// 5. zero-length non-nil []NodeSelectorRequirement matches no nodes also, just for simplicity
   344  	// 6. non-nil empty NodeSelectorRequirement is not allowed
   345  	if affinity != nil && affinity.NodeAffinity != nil {
   346  		nodeAffinity := affinity.NodeAffinity
   347  		// if no required NodeAffinity requirements, will do no-op, means select all nodes.
   348  		if nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil {
   349  			return true, nil
   350  		}
   351  		return corev1helpers.MatchNodeSelectorTerms(node, nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution)
   352  	}
   353  	return true, nil
   354  }