github.com/kubeshop/testkube@v1.17.23/pkg/scheduler/test_scheduler.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/pkg/errors"
    10  	v1 "k8s.io/api/core/v1"
    11  
    12  	testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3"
    13  	testsourcev1 "github.com/kubeshop/testkube-operator/api/testsource/v1"
    14  	"github.com/kubeshop/testkube-operator/pkg/secret"
    15  	"github.com/kubeshop/testkube/internal/common"
    16  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    17  	"github.com/kubeshop/testkube/pkg/executor"
    18  	"github.com/kubeshop/testkube/pkg/executor/client"
    19  	"github.com/kubeshop/testkube/pkg/logs/events"
    20  	testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests"
    21  	"github.com/kubeshop/testkube/pkg/tcl/checktcl"
    22  	"github.com/kubeshop/testkube/pkg/tcl/schedulertcl"
    23  	"github.com/kubeshop/testkube/pkg/workerpool"
    24  )
    25  
    26  const (
    27  	containerType       = "container"
    28  	gitCredentialPrefix = "git_credential_"
    29  )
    30  
    31  func (s *Scheduler) PrepareTestRequests(work []testsv3.Test, request testkube.ExecutionRequest) []workerpool.Request[
    32  	testkube.Test, testkube.ExecutionRequest, testkube.Execution] {
    33  	requests := make([]workerpool.Request[testkube.Test, testkube.ExecutionRequest, testkube.Execution], len(work))
    34  	for i := range work {
    35  		requests[i] = workerpool.Request[testkube.Test, testkube.ExecutionRequest, testkube.Execution]{
    36  			Object:  testsmapper.MapTestCRToAPI(work[i]),
    37  			Options: request,
    38  			ExecFn:  s.executeTest,
    39  		}
    40  	}
    41  
    42  	return requests
    43  }
    44  
    45  func (s *Scheduler) executeTest(ctx context.Context, test testkube.Test, request testkube.ExecutionRequest) (
    46  	execution testkube.Execution, err error) {
    47  	// generate random execution name in case there is no one set
    48  	// like for docker images
    49  	if request.Name == "" && test.ExecutionRequest != nil && test.ExecutionRequest.Name != "" {
    50  		request.Name = test.ExecutionRequest.Name
    51  	}
    52  
    53  	request.Number = s.getNextExecutionNumber(test.Name)
    54  	if request.Name == "" {
    55  		request.Name = fmt.Sprintf("%s-%d", test.Name, request.Number)
    56  	}
    57  
    58  	if request.TestSuiteName != "" {
    59  		request.Name = fmt.Sprintf("%s-%d", request.Name, request.Number)
    60  	}
    61  
    62  	// test name + test execution name should be unique
    63  	execution, _ = s.testResults.GetByNameAndTest(ctx, request.Name, test.Name)
    64  
    65  	if execution.Name == request.Name {
    66  		err := errors.Errorf("test execution with name %s already exists", request.Name)
    67  		return s.handleExecutionError(ctx, execution, "duplicate execution: %w", err)
    68  	}
    69  
    70  	secretUUID, err := s.testsClient.GetCurrentSecretUUID(test.Name)
    71  	if err != nil {
    72  		return s.handleExecutionError(ctx, execution, "can't get current secret uuid: %w", err)
    73  	}
    74  
    75  	request.TestSecretUUID = secretUUID
    76  	// merge available data into execution options test spec, executor spec, request, test id
    77  	options, err := s.getExecuteOptions(test.Namespace, test.Name, request)
    78  	if err != nil {
    79  		return s.handleExecutionError(ctx, execution, "can't get execute options: %w", err)
    80  	}
    81  
    82  	// store execution in storage, can be fetched from API now
    83  	execution, err = newExecutionFromExecutionOptions(&s.subscriptionChecker, options)
    84  	if err != nil {
    85  		return s.handleExecutionError(ctx, execution, "can't get new execution: %w", err)
    86  	}
    87  
    88  	options.ID = execution.Id
    89  
    90  	s.events.Notify(testkube.NewEventStartTest(&execution))
    91  
    92  	if err := s.createSecretsReferences(&execution, &options); err != nil {
    93  		return s.handleExecutionError(ctx, execution, "can't create secret variables `Secret` references: %w", err)
    94  	}
    95  
    96  	err = s.testResults.Insert(ctx, execution)
    97  	if err != nil {
    98  		return s.handleExecutionError(ctx, execution, "can't create new test execution, can't insert into storage: %w", err)
    99  	}
   100  
   101  	s.logger.Infow("calling executor with options", "executionId", execution.Id, "options", options.Request)
   102  
   103  	execution.Start()
   104  
   105  	// update storage with current execution status
   106  	err = s.testResults.StartExecution(ctx, execution.Id, execution.StartTime)
   107  	if err != nil {
   108  		return s.handleExecutionError(ctx, execution, "can't execute test, can't insert into storage error: %w", err)
   109  	}
   110  
   111  	// sync/async test execution
   112  	result, err := s.startTestExecution(ctx, options, &execution)
   113  
   114  	// set execution result to one created
   115  	execution.ExecutionResult = result
   116  
   117  	// update storage with current execution status
   118  	if uerr := s.testResults.UpdateResult(ctx, execution.Id, execution); uerr != nil {
   119  		return s.handleExecutionError(ctx, execution, "update execution error: %w", err)
   120  	}
   121  
   122  	if err != nil {
   123  		return s.handleExecutionError(ctx, execution, "test execution failed: %w", err)
   124  	}
   125  
   126  	s.logger.Infow("test started", "executionId", execution.Id, "status", execution.ExecutionResult.Status)
   127  
   128  	s.handleExecutionStart(ctx, execution)
   129  
   130  	return execution, nil
   131  }
   132  
   133  func (s *Scheduler) handleExecutionStart(ctx context.Context, execution testkube.Execution) {
   134  	// pass here all needed execution data to the log
   135  	if s.featureFlags.LogsV2 {
   136  
   137  		l := events.NewLog(fmt.Sprintf("starting execution %s (%s)", execution.Name, execution.Id)).
   138  			WithType("execution-config").
   139  			WithVersion(events.LogVersionV2).
   140  			WithSource("test-scheduler").
   141  			WithMetadataEntry("command", strings.Join(execution.Command, " ")).
   142  			WithMetadataEntry("argsmode", execution.ArgsMode).
   143  			WithMetadataEntry("args", strings.Join(execution.Args, " ")).
   144  			WithMetadataEntry("pre-run", execution.PreRunScript).
   145  			WithMetadataEntry("post-run", execution.PostRunScript)
   146  
   147  		s.logsStream.Push(ctx, execution.Id, l)
   148  	}
   149  }
   150  
   151  func (s *Scheduler) handleExecutionError(ctx context.Context, execution testkube.Execution, msgTpl string, err error) (testkube.Execution, error) {
   152  	// push error log to the log stream if logs v2 enabled
   153  	if s.featureFlags.LogsV2 {
   154  		l := events.NewLog(fmt.Sprintf(msgTpl, err)).
   155  			WithType("error").
   156  			WithVersion(events.LogVersionV2).
   157  			WithSource("test-scheduler")
   158  
   159  		s.logsStream.Push(ctx, execution.Id, l)
   160  
   161  	}
   162  
   163  	// notify events that execution failed
   164  	s.events.Notify(testkube.NewEventEndTestFailed(&execution))
   165  
   166  	return execution.Errw(execution.Id, msgTpl, err), nil
   167  }
   168  
   169  func (s *Scheduler) startTestExecution(ctx context.Context, options client.ExecuteOptions, execution *testkube.Execution) (result *testkube.ExecutionResult, err error) {
   170  	executor := s.getExecutor(options.TestName)
   171  	return executor.Execute(ctx, execution, options)
   172  }
   173  
   174  func (s *Scheduler) getExecutor(testName string) client.Executor {
   175  	testCR, err := s.testsClient.Get(testName)
   176  	if err != nil {
   177  		s.logger.Errorw("can't get test", "test", testName, "error", err)
   178  		return s.executor
   179  	}
   180  
   181  	executorCR, err := s.executorsClient.GetByType(testCR.Spec.Type_)
   182  	if err != nil {
   183  		s.logger.Errorw("can't get executor", "test", testName, "error", err)
   184  		return s.executor
   185  	}
   186  
   187  	switch executorCR.Spec.ExecutorType {
   188  	case containerType:
   189  		return s.containerExecutor
   190  	default:
   191  		return s.executor
   192  	}
   193  }
   194  
   195  func (s *Scheduler) getNextExecutionNumber(testName string) int32 {
   196  	number, err := s.testResults.GetNextExecutionNumber(context.Background(), testName)
   197  	if err != nil {
   198  		s.logger.Errorw("retrieving latest execution", "error", err)
   199  		return number
   200  	}
   201  
   202  	return number
   203  }
   204  
   205  // createSecretsReferences strips secrets from text and store it inside model as reference to secret
   206  func (s *Scheduler) createSecretsReferences(execution *testkube.Execution, options *client.ExecuteOptions) (err error) {
   207  	secrets := map[string]string{}
   208  	secretName := execution.Id + "-vars"
   209  
   210  	for k, v := range execution.Variables {
   211  		if v.IsSecret() {
   212  			obfuscated := execution.Variables[k]
   213  			if v.SecretRef != nil {
   214  				obfuscated.SecretRef = &testkube.SecretRef{
   215  					Namespace: execution.TestNamespace,
   216  					Name:      v.SecretRef.Name,
   217  					Key:       v.SecretRef.Key,
   218  				}
   219  			} else {
   220  				obfuscated.Value = ""
   221  				obfuscated.SecretRef = &testkube.SecretRef{
   222  					Namespace: execution.TestNamespace,
   223  					Name:      secretName,
   224  					Key:       v.Name,
   225  				}
   226  
   227  				secrets[v.Name] = v.Value
   228  			}
   229  
   230  			execution.Variables[k] = obfuscated
   231  		}
   232  	}
   233  
   234  	secretRefs := []*testkube.SecretRef{options.UsernameSecret, options.TokenSecret}
   235  	for _, secretRef := range secretRefs {
   236  		if secretRef == nil {
   237  			continue
   238  		}
   239  
   240  		if execution.TestNamespace == s.namespace || (secretRef.Name != secret.GetMetadataName(execution.TestName, client.SecretTest) &&
   241  			secretRef.Name != secret.GetMetadataName(execution.TestName, client.SecretSource)) {
   242  			continue
   243  		}
   244  
   245  		data, err := s.secretClient.Get(secretRef.Name)
   246  		if err != nil {
   247  			return err
   248  		}
   249  
   250  		value, ok := data[secretRef.Key]
   251  		if !ok {
   252  			return fmt.Errorf("secret key %s not found for secret %s", secretRef.Key, secretRef.Name)
   253  		}
   254  
   255  		secrets[gitCredentialPrefix+secretRef.Key] = value
   256  		secretRef.Name = secretName
   257  		secretRef.Key = gitCredentialPrefix + secretRef.Key
   258  	}
   259  
   260  	labels := map[string]string{"executionID": execution.Id, "testName": execution.TestName}
   261  
   262  	if len(secrets) > 0 {
   263  		return s.secretClient.Create(
   264  			secretName,
   265  			labels,
   266  			secrets,
   267  			execution.TestNamespace,
   268  		)
   269  	}
   270  
   271  	return nil
   272  }
   273  
   274  func newExecutionFromExecutionOptions(subscriptionChecker *checktcl.SubscriptionChecker, options client.ExecuteOptions) (testkube.Execution, error) {
   275  	execution := testkube.NewExecution(
   276  		options.Request.Id,
   277  		options.Namespace,
   278  		options.TestName,
   279  		options.Request.TestSuiteName,
   280  		options.Request.Name,
   281  		options.TestSpec.Type_,
   282  		int(options.Request.Number),
   283  		testsmapper.MapTestContentFromSpec(options.TestSpec.Content),
   284  		*testkube.NewRunningExecutionResult(),
   285  		options.Request.Variables,
   286  		options.Request.TestSecretUUID,
   287  		options.Request.TestSuiteSecretUUID,
   288  		common.MergeMaps(options.Labels, options.Request.ExecutionLabels),
   289  	)
   290  
   291  	execution.Envs = options.Request.Envs
   292  	execution.Command = options.Request.Command
   293  	execution.Args = options.Request.Args
   294  	execution.IsVariablesFileUploaded = options.Request.IsVariablesFileUploaded
   295  	execution.VariablesFile = options.Request.VariablesFile
   296  	execution.Uploads = options.Request.Uploads
   297  	execution.BucketName = options.Request.BucketName
   298  	execution.ArtifactRequest = options.Request.ArtifactRequest
   299  	execution.PreRunScript = options.Request.PreRunScript
   300  	execution.PostRunScript = options.Request.PostRunScript
   301  	execution.ExecutePostRunScriptBeforeScraping = options.Request.ExecutePostRunScriptBeforeScraping
   302  	execution.SourceScripts = options.Request.SourceScripts
   303  	execution.RunningContext = options.Request.RunningContext
   304  	execution.TestExecutionName = options.Request.TestExecutionName
   305  	execution.DownloadArtifactExecutionIDs = options.Request.DownloadArtifactExecutionIDs
   306  	execution.DownloadArtifactTestNames = options.Request.DownloadArtifactTestNames
   307  	execution.SlavePodRequest = options.Request.SlavePodRequest
   308  
   309  	// Pro edition only (tcl protected code)
   310  	if schedulertcl.HasExecutionNamespace(&options.Request) {
   311  		if err := subscriptionChecker.IsActiveOrgPlanEnterpriseForFeature("execution namespace"); err != nil {
   312  			return execution, err
   313  		}
   314  
   315  		execution = schedulertcl.NewExecutionFromExecutionOptions(options.Request, execution)
   316  	}
   317  
   318  	return execution, nil
   319  }
   320  
   321  func (s *Scheduler) getExecuteOptions(namespace, id string, request testkube.ExecutionRequest) (options client.ExecuteOptions, err error) {
   322  	// get test content from kubernetes CRs
   323  	testCR, err := s.testsClient.Get(id)
   324  	if err != nil {
   325  		return options, errors.Errorf("can't get test custom resource %v", err)
   326  	}
   327  
   328  	if testCR.Spec.Source != "" {
   329  		testSourceCR, err := s.testSourcesClient.Get(testCR.Spec.Source)
   330  		if err != nil {
   331  			return options, errors.Errorf("cannot get test source custom resource: %v", err)
   332  		}
   333  
   334  		testCR.Spec = mergeContents(testCR.Spec, testSourceCR.Spec)
   335  
   336  		if testSourceCR.Spec.Type_ == "" && testSourceCR.Spec.Repository.Type_ == "git" {
   337  			testCR.Spec.Content.Type_ = testsv3.TestContentType(testkube.TestContentTypeGit)
   338  		}
   339  	}
   340  
   341  	if request.ContentRequest != nil {
   342  		testCR.Spec = adjustContent(testCR.Spec, request.ContentRequest)
   343  	}
   344  
   345  	test := testsmapper.MapTestCRToAPI(*testCR)
   346  
   347  	request.Namespace = namespace
   348  	if test.ExecutionRequest != nil {
   349  		// Test variables lowest priority, then test suite, then test suite execution / test execution
   350  		request.Variables = mergeVariables(test.ExecutionRequest.Variables, request.Variables)
   351  
   352  		request.Envs = mergeEnvs(request.Envs, test.ExecutionRequest.Envs)
   353  		request.SecretEnvs = mergeEnvs(request.SecretEnvs, test.ExecutionRequest.SecretEnvs)
   354  		request.EnvConfigMaps = mergeEnvReferences(request.EnvConfigMaps, test.ExecutionRequest.EnvConfigMaps)
   355  		request.EnvSecrets = mergeEnvReferences(request.EnvSecrets, test.ExecutionRequest.EnvSecrets)
   356  
   357  		if request.VariablesFile == "" && test.ExecutionRequest.VariablesFile != "" {
   358  			request.VariablesFile = test.ExecutionRequest.VariablesFile
   359  			request.IsVariablesFileUploaded = test.ExecutionRequest.IsVariablesFileUploaded
   360  		}
   361  
   362  		var fields = []struct {
   363  			source      string
   364  			destination *string
   365  		}{
   366  			{
   367  				test.ExecutionRequest.HttpProxy,
   368  				&request.HttpProxy,
   369  			},
   370  			{
   371  				test.ExecutionRequest.HttpsProxy,
   372  				&request.HttpsProxy,
   373  			},
   374  			{
   375  				test.ExecutionRequest.JobTemplate,
   376  				&request.JobTemplate,
   377  			},
   378  			{
   379  				test.ExecutionRequest.JobTemplateReference,
   380  				&request.JobTemplateReference,
   381  			},
   382  			{
   383  				test.ExecutionRequest.PreRunScript,
   384  				&request.PreRunScript,
   385  			},
   386  			{
   387  				test.ExecutionRequest.PostRunScript,
   388  				&request.PostRunScript,
   389  			},
   390  			{
   391  				test.ExecutionRequest.ScraperTemplate,
   392  				&request.ScraperTemplate,
   393  			},
   394  			{
   395  				test.ExecutionRequest.ScraperTemplateReference,
   396  				&request.ScraperTemplateReference,
   397  			},
   398  			{
   399  				test.ExecutionRequest.PvcTemplate,
   400  				&request.PvcTemplate,
   401  			},
   402  			{
   403  				test.ExecutionRequest.PvcTemplateReference,
   404  				&request.PvcTemplateReference,
   405  			},
   406  			{
   407  				test.ExecutionRequest.ArgsMode,
   408  				&request.ArgsMode,
   409  			},
   410  		}
   411  
   412  		for _, field := range fields {
   413  			if *field.destination == "" && field.source != "" {
   414  				*field.destination = field.source
   415  			}
   416  		}
   417  
   418  		// Combine test executor args with execution args
   419  		if len(request.Command) == 0 {
   420  			request.Command = test.ExecutionRequest.Command
   421  		}
   422  
   423  		if len(request.Args) == 0 {
   424  			request.Args = test.ExecutionRequest.Args
   425  		}
   426  
   427  		if request.ActiveDeadlineSeconds == 0 && test.ExecutionRequest.ActiveDeadlineSeconds != 0 {
   428  			request.ActiveDeadlineSeconds = test.ExecutionRequest.ActiveDeadlineSeconds
   429  		}
   430  
   431  		if !request.ExecutePostRunScriptBeforeScraping && test.ExecutionRequest.ExecutePostRunScriptBeforeScraping {
   432  			request.ExecutePostRunScriptBeforeScraping = test.ExecutionRequest.ExecutePostRunScriptBeforeScraping
   433  		}
   434  
   435  		if !request.SourceScripts && test.ExecutionRequest.SourceScripts {
   436  			request.SourceScripts = test.ExecutionRequest.SourceScripts
   437  		}
   438  
   439  		request.ArtifactRequest = mergeArtifacts(request.ArtifactRequest, test.ExecutionRequest.ArtifactRequest)
   440  		if request.ArtifactRequest != nil && request.ArtifactRequest.VolumeMountPath == "" {
   441  			request.ArtifactRequest.VolumeMountPath = filepath.Join(executor.VolumeDir, "artifacts")
   442  		}
   443  
   444  		request.SlavePodRequest = mergeSlavePodRequests(request.SlavePodRequest, test.ExecutionRequest.SlavePodRequest)
   445  		s.logger.Infow("checking for negative test change", "test", test.Name, "negativeTest", request.NegativeTest, "isNegativeTestChangedOnRun", request.IsNegativeTestChangedOnRun)
   446  		if !request.IsNegativeTestChangedOnRun {
   447  			s.logger.Infow("setting negative test from test definition", "test", test.Name, "negativeTest", test.ExecutionRequest.NegativeTest)
   448  			request.NegativeTest = test.ExecutionRequest.NegativeTest
   449  		}
   450  
   451  		// Pro edition only (tcl protected code)
   452  		if schedulertcl.HasExecutionNamespace(test.ExecutionRequest) {
   453  			if err = s.subscriptionChecker.IsActiveOrgPlanEnterpriseForFeature("execution namespace"); err != nil {
   454  				return options, err
   455  			}
   456  
   457  			request = schedulertcl.GetExecuteOptions(test.ExecutionRequest, request)
   458  		}
   459  	}
   460  
   461  	// get executor from kubernetes CRs
   462  	executorCR, err := s.executorsClient.GetByType(testCR.Spec.Type_)
   463  	if err != nil {
   464  		return options, errors.Errorf("can't get executor spec: %v", err)
   465  	}
   466  
   467  	var usernameSecret, tokenSecret *testkube.SecretRef
   468  	var certificateSecret string
   469  	if test.Content != nil && test.Content.Repository != nil {
   470  		usernameSecret = test.Content.Repository.UsernameSecret
   471  		tokenSecret = test.Content.Repository.TokenSecret
   472  		certificateSecret = test.Content.Repository.CertificateSecret
   473  	}
   474  
   475  	var imagePullSecrets []string
   476  
   477  	if len(executorCR.Spec.ImagePullSecrets) != 0 {
   478  		imagePullSecrets = mapK8sImagePullSecrets(executorCR.Spec.ImagePullSecrets)
   479  	}
   480  
   481  	if testCR.Spec.ExecutionRequest != nil &&
   482  		len(testCR.Spec.ExecutionRequest.ImagePullSecrets) != 0 {
   483  		imagePullSecrets = mapK8sImagePullSecrets(testCR.Spec.ExecutionRequest.ImagePullSecrets)
   484  	}
   485  
   486  	if len(request.ImagePullSecrets) != 0 {
   487  		imagePullSecrets = mapImagePullSecrets(request.ImagePullSecrets)
   488  	}
   489  
   490  	configMapVars := make(map[string]testkube.Variable, 0)
   491  	for _, configMap := range request.EnvConfigMaps {
   492  		if configMap.Reference == nil || !configMap.MapToVariables {
   493  			continue
   494  		}
   495  
   496  		data, err := s.configMapClient.Get(context.Background(), configMap.Reference.Name, request.Namespace)
   497  		if err != nil {
   498  			return options, errors.Errorf("can't get config map: %v", err)
   499  		}
   500  
   501  		for key := range data {
   502  			configMapVars[key] = testkube.NewConfigMapVariableReference(key, configMap.Reference.Name, key)
   503  		}
   504  	}
   505  
   506  	if len(configMapVars) != 0 {
   507  		request.Variables = mergeVariables(configMapVars, request.Variables)
   508  	}
   509  
   510  	secretVars := make(map[string]testkube.Variable, 0)
   511  	for _, secret := range request.EnvSecrets {
   512  		if secret.Reference == nil || !secret.MapToVariables {
   513  			continue
   514  		}
   515  
   516  		data, err := s.secretClient.Get(secret.Reference.Name, request.Namespace)
   517  		if err != nil {
   518  			return options, errors.Errorf("can't get secret: %v", err)
   519  		}
   520  
   521  		for key := range data {
   522  			secretVars[key] = testkube.NewSecretVariableReference(key, secret.Reference.Name, key)
   523  		}
   524  	}
   525  
   526  	if len(secretVars) != 0 {
   527  		request.Variables = mergeVariables(secretVars, request.Variables)
   528  	}
   529  
   530  	if len(request.Command) == 0 {
   531  		request.Command = executorCR.Spec.Command
   532  	}
   533  
   534  	if request.ArgsMode == string(testkube.ArgsModeTypeAppend) || request.ArgsMode == "" {
   535  		request.Args = append(executorCR.Spec.Args, request.Args...)
   536  	}
   537  
   538  	if executorCR.Spec.UseDataDirAsWorkingDir {
   539  		if testCR.Spec.Content.Repository != nil && testCR.Spec.Content.Repository.WorkingDir == "" {
   540  			if executorCR.Spec.ExecutorType == containerType {
   541  				testCR.Spec.Content.Repository.WorkingDir = filepath.Join(executor.VolumeDir, "repo")
   542  			} else {
   543  				testCR.Spec.Content.Repository.WorkingDir = "/"
   544  			}
   545  		}
   546  	}
   547  
   548  	return client.ExecuteOptions{
   549  		TestName:             id,
   550  		Namespace:            request.Namespace,
   551  		TestSpec:             testCR.Spec,
   552  		ExecutorName:         executorCR.ObjectMeta.Name,
   553  		ExecutorSpec:         executorCR.Spec,
   554  		Request:              request,
   555  		Sync:                 request.Sync,
   556  		Labels:               testCR.Labels,
   557  		UsernameSecret:       usernameSecret,
   558  		TokenSecret:          tokenSecret,
   559  		RunnerCustomCASecret: s.runnerCustomCASecret,
   560  		CertificateSecret:    certificateSecret,
   561  		AgentAPITLSSecret:    s.agentAPITLSSecret,
   562  		ImagePullSecretNames: imagePullSecrets,
   563  		Features:             s.featureFlags,
   564  	}, nil
   565  }
   566  
   567  func mergeVariables(vars1 map[string]testkube.Variable, vars2 map[string]testkube.Variable) map[string]testkube.Variable {
   568  	variables := map[string]testkube.Variable{}
   569  	for k, v := range vars1 {
   570  		variables[k] = v
   571  	}
   572  
   573  	for k, v := range vars2 {
   574  		variables[k] = v
   575  	}
   576  
   577  	return variables
   578  }
   579  
   580  func mergeEnvs(envs1 map[string]string, envs2 map[string]string) map[string]string {
   581  	envs := map[string]string{}
   582  	for k, v := range envs1 {
   583  		envs[k] = v
   584  	}
   585  
   586  	for k, v := range envs2 {
   587  		envs[k] = v
   588  	}
   589  
   590  	return envs
   591  }
   592  
   593  func mergeContents(test testsv3.TestSpec, testSource testsourcev1.TestSourceSpec) testsv3.TestSpec {
   594  	if test.Content == nil {
   595  		test.Content = &testsv3.TestContent{}
   596  	}
   597  
   598  	if test.Content.Type_ == "" {
   599  		test.Content.Type_ = testsv3.TestContentType(testSource.Type_)
   600  	}
   601  
   602  	if test.Content.Data == "" {
   603  		test.Content.Data = testSource.Data
   604  	}
   605  
   606  	if test.Content.Uri == "" {
   607  		test.Content.Uri = testSource.Uri
   608  	}
   609  
   610  	if testSource.Repository != nil {
   611  		if test.Content.Repository == nil {
   612  			test.Content.Repository = &testsv3.Repository{}
   613  		}
   614  
   615  		if test.Content.Repository.UsernameSecret == nil && testSource.Repository.UsernameSecret != nil {
   616  			test.Content.Repository.UsernameSecret = &testsv3.SecretRef{
   617  				Name: testSource.Repository.UsernameSecret.Name,
   618  				Key:  testSource.Repository.UsernameSecret.Key,
   619  			}
   620  		}
   621  
   622  		if test.Content.Repository.TokenSecret == nil && testSource.Repository.TokenSecret != nil {
   623  			test.Content.Repository.TokenSecret = &testsv3.SecretRef{
   624  				Name: testSource.Repository.TokenSecret.Name,
   625  				Key:  testSource.Repository.TokenSecret.Key,
   626  			}
   627  		}
   628  
   629  		if test.Content.Repository.AuthType == "" {
   630  			test.Content.Repository.AuthType = testsv3.GitAuthType(testSource.Repository.AuthType)
   631  		}
   632  
   633  		var fields = []struct {
   634  			source      string
   635  			destination *string
   636  		}{
   637  			{
   638  				testSource.Repository.Type_,
   639  				&test.Content.Repository.Type_,
   640  			},
   641  			{
   642  				testSource.Repository.Uri,
   643  				&test.Content.Repository.Uri,
   644  			},
   645  			{
   646  				testSource.Repository.Branch,
   647  				&test.Content.Repository.Branch,
   648  			},
   649  			{
   650  				testSource.Repository.Commit,
   651  				&test.Content.Repository.Commit,
   652  			},
   653  			{
   654  				testSource.Repository.Path,
   655  				&test.Content.Repository.Path,
   656  			},
   657  			{
   658  				testSource.Repository.WorkingDir,
   659  				&test.Content.Repository.WorkingDir,
   660  			},
   661  			{
   662  				testSource.Repository.CertificateSecret,
   663  				&test.Content.Repository.CertificateSecret,
   664  			},
   665  		}
   666  
   667  		for _, field := range fields {
   668  			if *field.destination == "" {
   669  				*field.destination = field.source
   670  			}
   671  		}
   672  	}
   673  
   674  	return test
   675  }
   676  
   677  // TODO: generics
   678  func mapImagePullSecrets(secrets []testkube.LocalObjectReference) []string {
   679  	var res []string
   680  	for _, secret := range secrets {
   681  		res = append(res, secret.Name)
   682  	}
   683  
   684  	return res
   685  }
   686  
   687  func mapK8sImagePullSecrets(secrets []v1.LocalObjectReference) []string {
   688  	var res []string
   689  	for _, secret := range secrets {
   690  		res = append(res, secret.Name)
   691  	}
   692  
   693  	return res
   694  }
   695  
   696  func mergeArtifacts(artifactBase *testkube.ArtifactRequest, artifactAdjust *testkube.ArtifactRequest) *testkube.ArtifactRequest {
   697  	switch {
   698  	case artifactBase == nil && artifactAdjust == nil:
   699  		return nil
   700  	case artifactBase == nil && artifactAdjust != nil:
   701  		return artifactAdjust
   702  	case artifactBase != nil && artifactAdjust == nil:
   703  		return artifactBase
   704  	default:
   705  		artifactBase.Dirs = append(artifactBase.Dirs, artifactAdjust.Dirs...)
   706  		artifactBase.Masks = append(artifactBase.Masks, artifactAdjust.Masks...)
   707  
   708  		if !artifactBase.OmitFolderPerExecution && artifactAdjust.OmitFolderPerExecution {
   709  			artifactBase.OmitFolderPerExecution = artifactAdjust.OmitFolderPerExecution
   710  		}
   711  
   712  		if !artifactBase.SharedBetweenPods && artifactAdjust.SharedBetweenPods {
   713  			artifactBase.SharedBetweenPods = artifactAdjust.SharedBetweenPods
   714  		}
   715  
   716  		if !artifactBase.UseDefaultStorageClassName && artifactAdjust.UseDefaultStorageClassName {
   717  			artifactBase.UseDefaultStorageClassName = artifactAdjust.UseDefaultStorageClassName
   718  		}
   719  
   720  		var fields = []struct {
   721  			source      string
   722  			destination *string
   723  		}{
   724  			{
   725  				artifactAdjust.StorageClassName,
   726  				&artifactBase.StorageClassName,
   727  			},
   728  			{
   729  				artifactAdjust.VolumeMountPath,
   730  				&artifactBase.VolumeMountPath,
   731  			},
   732  			{
   733  				artifactAdjust.StorageBucket,
   734  				&artifactBase.StorageBucket,
   735  			},
   736  		}
   737  
   738  		for _, field := range fields {
   739  			if *field.destination == "" && field.source != "" {
   740  				*field.destination = field.source
   741  			}
   742  		}
   743  	}
   744  
   745  	return artifactBase
   746  }
   747  
   748  func adjustContent(test testsv3.TestSpec, content *testkube.TestContentRequest) testsv3.TestSpec {
   749  	if test.Content == nil {
   750  		return test
   751  	}
   752  
   753  	switch testkube.TestContentType(test.Content.Type_) {
   754  	case testkube.TestContentTypeGitFile, testkube.TestContentTypeGitDir, testkube.TestContentTypeGit:
   755  		if test.Content.Repository == nil {
   756  			return test
   757  		}
   758  
   759  		if content.Repository != nil {
   760  			var fields = []struct {
   761  				source      string
   762  				destination *string
   763  			}{
   764  				{
   765  					content.Repository.Branch,
   766  					&test.Content.Repository.Branch,
   767  				},
   768  				{
   769  					content.Repository.Commit,
   770  					&test.Content.Repository.Commit,
   771  				},
   772  				{
   773  					content.Repository.Path,
   774  					&test.Content.Repository.Path,
   775  				},
   776  				{
   777  					content.Repository.WorkingDir,
   778  					&test.Content.Repository.WorkingDir,
   779  				},
   780  			}
   781  
   782  			for _, field := range fields {
   783  				if field.source != "" {
   784  					*field.destination = field.source
   785  				}
   786  			}
   787  		}
   788  	}
   789  
   790  	return test
   791  }
   792  
   793  func mergeEnvReferences(envs1 []testkube.EnvReference, envs2 []testkube.EnvReference) []testkube.EnvReference {
   794  	envs := make(map[string]testkube.EnvReference, 0)
   795  	for i := range envs1 {
   796  		if envs1[i].Reference == nil {
   797  			continue
   798  		}
   799  
   800  		envs[envs1[i].Reference.Name] = envs1[i]
   801  	}
   802  
   803  	for i := range envs2 {
   804  		if envs2[i].Reference == nil {
   805  			continue
   806  		}
   807  
   808  		if value, ok := envs[envs2[i].Reference.Name]; !ok {
   809  			envs[envs2[i].Reference.Name] = envs2[i]
   810  		} else {
   811  			if !value.Mount {
   812  				value.Mount = envs2[i].Mount
   813  			}
   814  
   815  			if value.MountPath == "" {
   816  				value.MountPath = envs2[i].MountPath
   817  			}
   818  
   819  			if !value.MapToVariables {
   820  				value.MapToVariables = envs2[i].MapToVariables
   821  			}
   822  
   823  			envs[envs2[i].Reference.Name] = value
   824  		}
   825  	}
   826  
   827  	res := make([]testkube.EnvReference, 0)
   828  	for key := range envs {
   829  		res = append(res, envs[key])
   830  	}
   831  
   832  	return res
   833  }
   834  
   835  func mergeSlavePodRequests(podBase *testkube.PodRequest, podAdjust *testkube.PodRequest) *testkube.PodRequest {
   836  	switch {
   837  	case podBase == nil && podAdjust == nil:
   838  		return nil
   839  	case podBase == nil && podAdjust != nil:
   840  		return podAdjust
   841  	case podBase != nil && podAdjust == nil:
   842  		return podBase
   843  	default:
   844  		var fields = []struct {
   845  			source      string
   846  			destination *string
   847  		}{
   848  			{
   849  				podAdjust.PodTemplate,
   850  				&podBase.PodTemplate,
   851  			},
   852  			{
   853  				podAdjust.PodTemplateReference,
   854  				&podBase.PodTemplateReference,
   855  			},
   856  		}
   857  
   858  		for _, field := range fields {
   859  			if *field.destination == "" && field.source != "" {
   860  				*field.destination = field.source
   861  			}
   862  		}
   863  
   864  		if podBase.Resources == nil && podAdjust.Resources != nil {
   865  			podBase.Resources = podAdjust.Resources
   866  			return podBase
   867  		}
   868  
   869  		if podBase.Resources != nil && podAdjust.Resources != nil {
   870  			if podBase.Resources.Requests == nil && podAdjust.Resources.Requests != nil {
   871  				podBase.Resources.Requests = podAdjust.Resources.Requests
   872  			} else if podBase.Resources.Requests != nil && podAdjust.Resources.Requests != nil {
   873  				if podBase.Resources.Requests.Cpu == "" && podAdjust.Resources.Requests.Cpu != "" {
   874  					podBase.Resources.Requests.Cpu = podAdjust.Resources.Requests.Cpu
   875  				}
   876  
   877  				if podBase.Resources.Requests.Memory == "" && podAdjust.Resources.Requests.Memory != "" {
   878  					podBase.Resources.Requests.Memory = podAdjust.Resources.Requests.Memory
   879  				}
   880  			}
   881  
   882  			if podBase.Resources.Limits == nil && podAdjust.Resources.Limits != nil {
   883  				podBase.Resources.Limits = podAdjust.Resources.Limits
   884  			} else if podBase.Resources.Limits != nil && podAdjust.Resources.Limits != nil {
   885  				if podBase.Resources.Limits.Cpu == "" && podAdjust.Resources.Limits.Cpu != "" {
   886  					podBase.Resources.Limits.Cpu = podAdjust.Resources.Limits.Cpu
   887  				}
   888  
   889  				if podBase.Resources.Limits.Memory == "" && podAdjust.Resources.Limits.Memory != "" {
   890  					podBase.Resources.Limits.Memory = podAdjust.Resources.Limits.Memory
   891  				}
   892  			}
   893  		}
   894  
   895  	}
   896  
   897  	return podBase
   898  }