k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/test/simple_test_executor.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 test
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"path"
    23  	"strings"
    24  	"time"
    25  
    26  	"k8s.io/perf-tests/clusterloader2/pkg/measurement"
    27  
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	"k8s.io/klog/v2"
    34  	"k8s.io/perf-tests/clusterloader2/api"
    35  	"k8s.io/perf-tests/clusterloader2/pkg/config"
    36  	"k8s.io/perf-tests/clusterloader2/pkg/errors"
    37  	"k8s.io/perf-tests/clusterloader2/pkg/measurement/util/runtimeobjects"
    38  	"k8s.io/perf-tests/clusterloader2/pkg/state"
    39  	"k8s.io/perf-tests/clusterloader2/pkg/util"
    40  )
    41  
    42  const (
    43  	baseNamePlaceholder  = "BaseName"
    44  	indexPlaceholder     = "Index"
    45  	namePlaceholder      = "Name"
    46  	namespacePlaceholder = "Namespace"
    47  )
    48  
    49  type simpleExecutor struct{}
    50  
    51  func createSimpleExecutor() Executor {
    52  	return &simpleExecutor{}
    53  }
    54  
    55  // ExecuteTest executes test based on provided configuration.
    56  func (ste *simpleExecutor) ExecuteTest(ctx Context, conf *api.Config) *errors.ErrorList {
    57  	ctx.GetClusterFramework().SetAutomanagedNamespacePrefix(conf.Namespace.Prefix)
    58  	klog.V(2).Infof("AutomanagedNamespacePrefix: %s", ctx.GetClusterFramework().GetAutomanagedNamespacePrefix())
    59  
    60  	defer cleanupResources(ctx, conf)
    61  	ctx.GetFactory().Init(conf.TuningSets)
    62  
    63  	stopCh := make(chan struct{})
    64  	if conf.ChaosMonkey.ExcludedNodes == nil {
    65  		conf.ChaosMonkey.ExcludedNodes = sets.NewString()
    66  	}
    67  	chaosMonkeyWaitGroup, err := ctx.GetChaosMonkey().Init(conf.ChaosMonkey, stopCh)
    68  	if err != nil {
    69  		close(stopCh)
    70  		return errors.NewErrorList(fmt.Errorf("error while creating chaos monkey: %v", err))
    71  	}
    72  	if err := ste.prepareTestNamespaces(ctx, conf); err != nil {
    73  		return errors.NewErrorList(fmt.Errorf("error while preparing test namespaces: %w", err))
    74  	}
    75  	errList := ste.ExecuteTestSteps(ctx, conf.Steps)
    76  	close(stopCh)
    77  
    78  	if chaosMonkeyWaitGroup != nil {
    79  		// Wait for the Chaos Monkey subroutine to end
    80  		klog.V(2).Info("Waiting for the chaos monkey subroutine to end...")
    81  		chaosMonkeyWaitGroup.Wait()
    82  		klog.V(2).Info("Chaos monkey ended.")
    83  	}
    84  
    85  	for _, summary := range ctx.GetManager().GetSummaries() {
    86  		if ctx.GetClusterLoaderConfig().ReportDir == "" {
    87  			klog.V(2).Infof("%v: %v", summary.SummaryName(), summary.SummaryContent())
    88  		} else {
    89  			testDistinctor := ""
    90  			if ctx.GetTestScenario().Identifier != "" {
    91  				testDistinctor = "_" + ctx.GetTestScenario().Identifier
    92  			}
    93  			// TODO(krzysied): Remember to keep original filename style for backward compatibility.
    94  			fileName := strings.Join([]string{summary.SummaryName(), conf.Name + testDistinctor, summary.SummaryTime().Format(time.RFC3339)}, "_")
    95  			filePath := path.Join(ctx.GetClusterLoaderConfig().ReportDir, strings.Join([]string{fileName, summary.SummaryExt()}, "."))
    96  			if err := ioutil.WriteFile(filePath, []byte(summary.SummaryContent()), 0644); err != nil {
    97  				errList.Append(fmt.Errorf("writing to file %v error: %v", filePath, err))
    98  				continue
    99  			}
   100  		}
   101  	}
   102  	klog.V(2).Infof(ctx.GetChaosMonkey().Summary())
   103  	return errList
   104  }
   105  
   106  // prepareTestNamespaces prepares k8s namespaces for the test.
   107  func (ste *simpleExecutor) prepareTestNamespaces(ctx Context, conf *api.Config) error {
   108  	automanagedNamespacesCurrentPrefixList, staleNamespaces, err := ctx.GetClusterFramework().ListAutomanagedNamespaces()
   109  	if err != nil {
   110  		return fmt.Errorf("automanaged namespaces listing failed: %w", err)
   111  	}
   112  	if len(automanagedNamespacesCurrentPrefixList) > 0 && *conf.Namespace.EnableExistingNamespaces == false {
   113  		return fmt.Errorf("pre-existing automanaged namespaces found")
   114  	}
   115  	var deleteStaleNS = *conf.Namespace.DeleteStaleNamespaces
   116  	if len(staleNamespaces) > 0 && deleteStaleNS {
   117  		klog.Warning("stale automanaged namespaces found")
   118  		if errList := ctx.GetClusterFramework().DeleteNamespaces(staleNamespaces, conf.Namespace.DeleteNamespaceTimeout.ToTimeDuration()); !errList.IsEmpty() {
   119  			klog.Errorf("stale automanaged namespaces cleanup error: %s", errList.String())
   120  		}
   121  	}
   122  	if err := ctx.GetClusterFramework().CreateAutomanagedNamespaces(int(conf.Namespace.Number), *conf.Namespace.EnableExistingNamespaces, *conf.Namespace.DeleteAutomanagedNamespaces); err != nil {
   123  		return fmt.Errorf("automanaged namespaces creation failed: %w", err)
   124  	}
   125  	return nil
   126  }
   127  
   128  // ExecuteTestSteps executes all test steps provided in configuration
   129  func (ste *simpleExecutor) ExecuteTestSteps(ctx Context, steps []*api.Step) *errors.ErrorList {
   130  	errList := errors.NewErrorList()
   131  	for i, step := range steps {
   132  		namePrefix := step.Name
   133  		if namePrefix == "" {
   134  			namePrefix = "[autogenerated, please name your step in the test config]"
   135  		}
   136  		step.Name = fmt.Sprintf("[step: %02d] %s", i+1, namePrefix)
   137  		if stepErrList := ste.ExecuteStep(ctx, step); !stepErrList.IsEmpty() {
   138  			errList.Concat(stepErrList)
   139  			if isErrsCritical(stepErrList) {
   140  				return errList
   141  			}
   142  		}
   143  	}
   144  	return errList
   145  }
   146  
   147  // ExecuteStep executes single test step based on provided step configuration.
   148  func (ste *simpleExecutor) ExecuteStep(ctx Context, step *api.Step) *errors.ErrorList {
   149  	klog.V(2).Infof("Step %q started", step.Name)
   150  	var wg wait.Group
   151  	stepResults := NewStepResult(step.Name)
   152  
   153  	// We already have validation so we know that either Measurements or Phases is non-empty.
   154  	for i := range step.Measurements {
   155  		currentMeasurement := step.Measurements[i]
   156  		substepName := fmt.Sprintf("[%02d] - %s", i, currentMeasurement.Identifier)
   157  		substepID := i
   158  		wg.Start(func() {
   159  			errList := measurement.Execute(ctx.GetManager(), currentMeasurement)
   160  			stepResults.AddSubStepResult(substepName, substepID, errList)
   161  		})
   162  	}
   163  	for i := range step.Phases {
   164  		phase := step.Phases[i]
   165  		wg.Start(func() {
   166  			errList := ste.ExecutePhase(ctx, phase)
   167  			stepResults.AddStepError(errList)
   168  		})
   169  	}
   170  	wg.Wait()
   171  	klog.V(2).Infof("Step %q ended", step.Name)
   172  	allErrors := stepResults.GetAllErrors()
   173  	if !allErrors.IsEmpty() {
   174  		klog.Warningf("Got errors during step execution: %v", allErrors)
   175  	}
   176  	ctx.GetTestReporter().ReportTestStep(stepResults)
   177  	return allErrors
   178  }
   179  
   180  // ExecutePhase executes single test phase based on provided phase configuration.
   181  func (ste *simpleExecutor) ExecutePhase(ctx Context, phase *api.Phase) *errors.ErrorList {
   182  	errList := errors.NewErrorList()
   183  	nsList := phase.NamespaceList
   184  	if nsList == nil {
   185  		nsList = createNamespacesList(ctx, phase.NamespaceRange)
   186  	}
   187  	tuningSet, err := ctx.GetFactory().CreateTuningSet(phase.TuningSet)
   188  	if err != nil {
   189  		return errors.NewErrorList(fmt.Errorf("tuning set creation error: %v", err))
   190  	}
   191  
   192  	var actions []func()
   193  	for namespaceIndex := range nsList {
   194  		nsName := nsList[namespaceIndex]
   195  		instancesStates := make([]*state.InstancesState, 0)
   196  		// Updating state (DesiredReplicaCount) of every object in object bundle.
   197  		for j := range phase.ObjectBundle {
   198  			id, err := getIdentifier(ctx, phase.ObjectBundle[j])
   199  			if err != nil {
   200  				errList.Append(err)
   201  				return errList
   202  			}
   203  			instances, exists := ctx.GetState().GetNamespacesState().Get(nsName, id)
   204  			if !exists {
   205  				currentReplicaCount, err := getReplicaCountOfNewObject(ctx, nsName, phase.ObjectBundle[j])
   206  				if err != nil {
   207  					errList.Append(err)
   208  					return errList
   209  				}
   210  				instances = &state.InstancesState{
   211  					DesiredReplicaCount: 0,
   212  					CurrentReplicaCount: currentReplicaCount,
   213  					Object:              phase.ObjectBundle[j],
   214  				}
   215  			}
   216  			instances.DesiredReplicaCount = phase.ReplicasPerNamespace
   217  			ctx.GetState().GetNamespacesState().Set(nsName, id, instances)
   218  			instancesStates = append(instancesStates, instances)
   219  		}
   220  
   221  		if err := verifyBundleCorrectness(instancesStates); err != nil {
   222  			klog.Errorf("Skipping phase. Incorrect bundle in phase: %+v", *phase)
   223  			return errors.NewErrorList(err)
   224  		}
   225  
   226  		if len(instancesStates) == 0 {
   227  			return nil
   228  		}
   229  
   230  		// Deleting objects with index greater or equal requested replicas per namespace number.
   231  		// Objects will be deleted in reversed order.
   232  		for replicaCounter := phase.ReplicasPerNamespace; replicaCounter < instancesStates[0].CurrentReplicaCount; replicaCounter++ {
   233  			replicaIndex := replicaCounter
   234  			actions = append(actions, func() {
   235  				for j := len(phase.ObjectBundle) - 1; j >= 0; j-- {
   236  					if replicaIndex < instancesStates[j].CurrentReplicaCount {
   237  						if objectErrList := ste.ExecuteObject(ctx, phase.ObjectBundle[j], nsName, replicaIndex, deleteObject); !objectErrList.IsEmpty() {
   238  							errList.Concat(objectErrList)
   239  						}
   240  					}
   241  				}
   242  			})
   243  		}
   244  
   245  		// Updating objects when desired replicas per namespace equals current replica count.
   246  		if instancesStates[0].CurrentReplicaCount == phase.ReplicasPerNamespace {
   247  			for replicaCounter := int32(0); replicaCounter < phase.ReplicasPerNamespace; replicaCounter++ {
   248  				replicaIndex := replicaCounter
   249  				actions = append(actions, func() {
   250  					for j := range phase.ObjectBundle {
   251  						if objectErrList := ste.ExecuteObject(ctx, phase.ObjectBundle[j], nsName, replicaIndex, patchObject); !objectErrList.IsEmpty() {
   252  							errList.Concat(objectErrList)
   253  							// If error then skip this bundle
   254  							break
   255  						}
   256  					}
   257  				})
   258  			}
   259  		}
   260  
   261  		// Adding objects with index greater than current replica count and lesser than desired replicas per namespace.
   262  		for replicaCounter := instancesStates[0].CurrentReplicaCount; replicaCounter < phase.ReplicasPerNamespace; replicaCounter++ {
   263  			replicaIndex := replicaCounter
   264  			actions = append(actions, func() {
   265  				for j := range phase.ObjectBundle {
   266  					if objectErrList := ste.ExecuteObject(ctx, phase.ObjectBundle[j], nsName, replicaIndex, createObject); !objectErrList.IsEmpty() {
   267  						errList.Concat(objectErrList)
   268  						// If error then skip this bundle
   269  						break
   270  					}
   271  				}
   272  			})
   273  		}
   274  
   275  		// Updating state (CurrentReplicaCount) of every object in object bundle.
   276  		defer func() {
   277  			for j := range phase.ObjectBundle {
   278  				id, _ := getIdentifier(ctx, phase.ObjectBundle[j])
   279  				instancesStates[j].CurrentReplicaCount = instancesStates[j].DesiredReplicaCount
   280  				ctx.GetState().GetNamespacesState().Set(nsName, id, instancesStates[j])
   281  			}
   282  		}()
   283  
   284  	}
   285  	tuningSet.Execute(actions)
   286  	return errList
   287  }
   288  
   289  // ExecuteObject executes single test object operation based on provided object configuration.
   290  func (ste *simpleExecutor) ExecuteObject(ctx Context, object *api.Object, namespace string, replicaIndex int32, operation OperationType) *errors.ErrorList {
   291  	objName := fmt.Sprintf("%v-%d", object.Basename, replicaIndex)
   292  	var err error
   293  	var obj *unstructured.Unstructured
   294  	switch operation {
   295  	case createObject, patchObject:
   296  		mapping := ctx.GetTemplateMappingCopy()
   297  		if object.TemplateFillMap != nil {
   298  			util.CopyMap(object.TemplateFillMap, mapping)
   299  		}
   300  		mapping[baseNamePlaceholder] = object.Basename
   301  		mapping[indexPlaceholder] = replicaIndex
   302  		mapping[namePlaceholder] = objName
   303  		mapping[namespacePlaceholder] = namespace
   304  		obj, err = ctx.GetTemplateProvider().TemplateToObject(object.ObjectTemplatePath, mapping)
   305  		if err != nil && err != config.ErrorEmptyFile {
   306  			return errors.NewErrorList(fmt.Errorf("reading template (%v) error: %v", object.ObjectTemplatePath, err))
   307  		}
   308  	case deleteObject:
   309  		obj, err = ctx.GetTemplateProvider().RawToObject(object.ObjectTemplatePath)
   310  		if err != nil && err != config.ErrorEmptyFile {
   311  			return errors.NewErrorList(fmt.Errorf("reading template (%v) for deletion error: %v", object.ObjectTemplatePath, err))
   312  		}
   313  	default:
   314  		return errors.NewErrorList(fmt.Errorf("unsupported operation %v for namespace %v object %v", operation, namespace, objName))
   315  	}
   316  	errList := errors.NewErrorList()
   317  	if err == config.ErrorEmptyFile {
   318  		return errList
   319  	}
   320  	gvk := obj.GroupVersionKind()
   321  	switch operation {
   322  	case createObject:
   323  		if err := ctx.GetClusterFramework().CreateObject(namespace, objName, obj); err != nil {
   324  			errList.Append(fmt.Errorf("namespace %v object %v creation error: %v", namespace, objName, err))
   325  		}
   326  	case patchObject:
   327  		if err := ctx.GetClusterFramework().PatchObject(namespace, objName, obj); err != nil {
   328  			errList.Append(fmt.Errorf("namespace %v object %v updating error: %v", namespace, objName, err))
   329  		}
   330  	case deleteObject:
   331  		if err := ctx.GetClusterFramework().DeleteObject(gvk, namespace, objName); err != nil {
   332  			errList.Append(fmt.Errorf("namespace %v object %v deletion error: %v", namespace, objName, err))
   333  		}
   334  	}
   335  	return errList
   336  }
   337  
   338  // verifyBundleCorrectness checks if all bundle objects have the same replica count.
   339  func verifyBundleCorrectness(instancesStates []*state.InstancesState) error {
   340  	const uninitialized int32 = -1
   341  	expectedReplicaCount := uninitialized
   342  	for j := range instancesStates {
   343  		if expectedReplicaCount != uninitialized && instancesStates[j].CurrentReplicaCount != expectedReplicaCount {
   344  			return fmt.Errorf("bundle error: %s has %d replicas while %s has %d",
   345  				instancesStates[j].Object.Basename,
   346  				instancesStates[j].CurrentReplicaCount,
   347  				instancesStates[j-1].Object.Basename,
   348  				instancesStates[j-1].CurrentReplicaCount)
   349  		}
   350  		expectedReplicaCount = instancesStates[j].CurrentReplicaCount
   351  	}
   352  	return nil
   353  }
   354  
   355  func getIdentifier(ctx Context, object *api.Object) (state.InstancesIdentifier, error) {
   356  	obj, err := ctx.GetTemplateProvider().RawToObject(object.ObjectTemplatePath)
   357  	if err != nil {
   358  		return state.InstancesIdentifier{}, fmt.Errorf("reading template (%v) for identifier error: %v", object.ObjectTemplatePath, err)
   359  	}
   360  	gvk := obj.GroupVersionKind()
   361  	return state.InstancesIdentifier{
   362  		Basename:   object.Basename,
   363  		ObjectKind: gvk.Kind,
   364  		APIGroup:   gvk.Group,
   365  	}, nil
   366  }
   367  
   368  func createNamespacesList(ctx Context, namespaceRange *api.NamespaceRange) []string {
   369  	if namespaceRange == nil {
   370  		// Returns "" which represents cluster level.
   371  		return []string{""}
   372  	}
   373  
   374  	nsList := make([]string, 0)
   375  	nsBasename := ctx.GetClusterFramework().GetAutomanagedNamespacePrefix()
   376  	if namespaceRange.Basename != nil {
   377  		nsBasename = *namespaceRange.Basename
   378  	}
   379  
   380  	for i := namespaceRange.Min; i <= namespaceRange.Max; i++ {
   381  		nsList = append(nsList, fmt.Sprintf("%v-%d", nsBasename, i))
   382  	}
   383  	return nsList
   384  }
   385  
   386  func isErrsCritical(*errors.ErrorList) bool {
   387  	// TODO: define critical errors
   388  	return false
   389  }
   390  
   391  func cleanupResources(ctx Context, conf *api.Config) {
   392  	cleanupStartTime := time.Now()
   393  	ctx.GetManager().Dispose()
   394  	if *conf.Namespace.DeleteAutomanagedNamespaces {
   395  		if errList := ctx.GetClusterFramework().DeleteAutomanagedNamespaces(conf.Namespace.DeleteNamespaceTimeout.ToTimeDuration()); !errList.IsEmpty() {
   396  			klog.Errorf("Resource cleanup error: %v", errList.String())
   397  			return
   398  		}
   399  	}
   400  	klog.V(2).Infof("Resources cleanup time: %v", time.Since(cleanupStartTime))
   401  }
   402  
   403  func getReplicaCountOfNewObject(ctx Context, namespace string, object *api.Object) (int32, error) {
   404  	if object.ListUnknownObjectOptions == nil {
   405  		return 0, nil
   406  	}
   407  	klog.V(4).Infof("%s: new object detected, will list objects in order to find num replicas", object.Basename)
   408  	selector, err := metav1.LabelSelectorAsSelector(object.ListUnknownObjectOptions.LabelSelector)
   409  	if err != nil {
   410  		return 0, err
   411  	}
   412  	obj, err := ctx.GetTemplateProvider().RawToObject(object.ObjectTemplatePath)
   413  	if err != nil {
   414  		return 0, err
   415  	}
   416  	gvk := obj.GroupVersionKind()
   417  	gvr, _ := meta.UnsafeGuessKindToResource(gvk)
   418  	replicaCount, err := runtimeobjects.GetNumObjectsMatchingSelector(
   419  		ctx.GetClusterFramework().GetDynamicClients().GetClient(),
   420  		namespace,
   421  		gvr,
   422  		selector)
   423  	if err != nil {
   424  		return 0, nil
   425  	}
   426  	klog.V(4).Infof("%s: found %d replicas", object.Basename, replicaCount)
   427  	return int32(replicaCount), nil
   428  }