github.com/kubeshop/testkube@v1.17.23/pkg/executor/containerexecutor/containerexecutor.go (about)

     1  package containerexecutor
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"time"
     8  
     9  	"github.com/pkg/errors"
    10  
    11  	"github.com/kubeshop/testkube/pkg/featureflags"
    12  	"github.com/kubeshop/testkube/pkg/imageinspector"
    13  	"github.com/kubeshop/testkube/pkg/repository/config"
    14  	"github.com/kubeshop/testkube/pkg/secret"
    15  	"github.com/kubeshop/testkube/pkg/utils"
    16  
    17  	"github.com/kubeshop/testkube/pkg/repository/result"
    18  
    19  	"github.com/kubeshop/testkube/pkg/version"
    20  
    21  	"go.uber.org/zap"
    22  	corev1 "k8s.io/api/core/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/util/wait"
    25  	"k8s.io/client-go/kubernetes"
    26  
    27  	executorv1 "github.com/kubeshop/testkube-operator/api/executor/v1"
    28  	executorsclientv1 "github.com/kubeshop/testkube-operator/pkg/client/executors/v1"
    29  	templatesv1 "github.com/kubeshop/testkube-operator/pkg/client/templates/v1"
    30  	testexecutionsv1 "github.com/kubeshop/testkube-operator/pkg/client/testexecutions/v1"
    31  	testsv3 "github.com/kubeshop/testkube-operator/pkg/client/tests/v3"
    32  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    33  	"github.com/kubeshop/testkube/pkg/executor"
    34  	"github.com/kubeshop/testkube/pkg/executor/client"
    35  	"github.com/kubeshop/testkube/pkg/executor/output"
    36  	"github.com/kubeshop/testkube/pkg/k8sclient"
    37  	"github.com/kubeshop/testkube/pkg/log"
    38  	logsclient "github.com/kubeshop/testkube/pkg/logs/client"
    39  	testexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testexecutions"
    40  	testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests"
    41  	"github.com/kubeshop/testkube/pkg/telemetry"
    42  )
    43  
    44  const (
    45  	// ScraperPodSuffix contains scraper pod suffix
    46  	ScraperPodSuffix = "-scraper"
    47  
    48  	pollTimeout             = 24 * time.Hour
    49  	pollInterval            = 200 * time.Millisecond
    50  	jobDefaultDelaySeconds  = 180
    51  	jobArtifactDelaySeconds = 90
    52  	repoPath                = "/data/repo"
    53  
    54  	logsChannelBuffer = 1000
    55  )
    56  
    57  type EventEmitter interface {
    58  	Notify(event testkube.Event)
    59  }
    60  
    61  // TODO: remove duplicated code that was done when created container executor
    62  
    63  // NewContainerExecutor creates new job executor
    64  func NewContainerExecutor(
    65  	repo result.Repository,
    66  	images executor.Images,
    67  	templates executor.Templates,
    68  	imageInspector imageinspector.Inspector,
    69  	serviceAccountNames map[string]string,
    70  	metrics ExecutionMetric,
    71  	emiter EventEmitter,
    72  	configMap config.Repository,
    73  	executorsClient executorsclientv1.Interface,
    74  	testsClient testsv3.Interface,
    75  	testExecutionsClient testexecutionsv1.Interface,
    76  	templatesClient templatesv1.Interface,
    77  	registry string,
    78  	podStartTimeout time.Duration,
    79  	clusterID string,
    80  	dashboardURI string,
    81  	apiURI string,
    82  	natsUri string,
    83  	debug bool,
    84  	logsStream logsclient.Stream,
    85  	features featureflags.FeatureFlags,
    86  	defaultStorageClassName string,
    87  ) (client *ContainerExecutor, err error) {
    88  	clientSet, err := k8sclient.ConnectToK8s()
    89  	if err != nil {
    90  		return client, err
    91  	}
    92  
    93  	if serviceAccountNames == nil {
    94  		serviceAccountNames = make(map[string]string)
    95  	}
    96  
    97  	return &ContainerExecutor{
    98  		clientSet:               clientSet,
    99  		repository:              repo,
   100  		log:                     log.DefaultLogger,
   101  		images:                  images,
   102  		templates:               templates,
   103  		imageInspector:          imageInspector,
   104  		configMap:               configMap,
   105  		serviceAccountNames:     serviceAccountNames,
   106  		metrics:                 metrics,
   107  		emitter:                 emiter,
   108  		testsClient:             testsClient,
   109  		executorsClient:         executorsClient,
   110  		testExecutionsClient:    testExecutionsClient,
   111  		templatesClient:         templatesClient,
   112  		registry:                registry,
   113  		podStartTimeout:         podStartTimeout,
   114  		clusterID:               clusterID,
   115  		dashboardURI:            dashboardURI,
   116  		apiURI:                  apiURI,
   117  		natsURI:                 natsUri,
   118  		debug:                   debug,
   119  		logsStream:              logsStream,
   120  		features:                features,
   121  		defaultStorageClassName: defaultStorageClassName,
   122  	}, nil
   123  }
   124  
   125  type ExecutionMetric interface {
   126  	IncAndObserveExecuteTest(execution testkube.Execution, dashboardURI string)
   127  }
   128  
   129  // ContainerExecutor is container for managing job executor dependencies
   130  type ContainerExecutor struct {
   131  	repository              result.Repository
   132  	log                     *zap.SugaredLogger
   133  	clientSet               kubernetes.Interface
   134  	images                  executor.Images
   135  	templates               executor.Templates
   136  	imageInspector          imageinspector.Inspector
   137  	metrics                 ExecutionMetric
   138  	emitter                 EventEmitter
   139  	configMap               config.Repository
   140  	serviceAccountNames     map[string]string
   141  	testsClient             testsv3.Interface
   142  	executorsClient         executorsclientv1.Interface
   143  	testExecutionsClient    testexecutionsv1.Interface
   144  	templatesClient         templatesv1.Interface
   145  	registry                string
   146  	podStartTimeout         time.Duration
   147  	clusterID               string
   148  	dashboardURI            string
   149  	apiURI                  string
   150  	natsURI                 string
   151  	debug                   bool
   152  	logsStream              logsclient.Stream
   153  	features                featureflags.FeatureFlags
   154  	defaultStorageClassName string
   155  }
   156  
   157  type JobOptions struct {
   158  	Name                      string
   159  	Namespace                 string
   160  	Image                     string
   161  	ImagePullSecrets          []string
   162  	Command                   []string
   163  	Args                      []string
   164  	WorkingDir                string
   165  	Jsn                       string
   166  	TestName                  string
   167  	InitImage                 string
   168  	ScraperImage              string
   169  	JobTemplate               string
   170  	ScraperTemplate           string
   171  	PvcTemplate               string
   172  	SecretEnvs                map[string]string
   173  	Envs                      map[string]string
   174  	HTTPProxy                 string
   175  	HTTPSProxy                string
   176  	UsernameSecret            *testkube.SecretRef
   177  	TokenSecret               *testkube.SecretRef
   178  	RunnerCustomCASecret      string
   179  	CertificateSecret         string
   180  	AgentAPITLSSecret         string
   181  	Variables                 map[string]testkube.Variable
   182  	ActiveDeadlineSeconds     int64
   183  	ArtifactRequest           *testkube.ArtifactRequest
   184  	ServiceAccountName        string
   185  	DelaySeconds              int
   186  	JobTemplateExtensions     string
   187  	ScraperTemplateExtensions string
   188  	PvcTemplateExtensions     string
   189  	EnvConfigMaps             []testkube.EnvReference
   190  	EnvSecrets                []testkube.EnvReference
   191  	Labels                    map[string]string
   192  	Registry                  string
   193  	ClusterID                 string
   194  	ExecutionNumber           int32
   195  	ContextType               string
   196  	ContextData               string
   197  	Debug                     bool
   198  	LogSidecarImage           string
   199  	NatsUri                   string
   200  	APIURI                    string
   201  	Features                  featureflags.FeatureFlags
   202  }
   203  
   204  // Logs returns job logs stream channel using kubernetes api
   205  func (c *ContainerExecutor) Logs(ctx context.Context, id, namespace string) (out chan output.Output, err error) {
   206  	out = make(chan output.Output, logsChannelBuffer)
   207  
   208  	go func() {
   209  		defer func() {
   210  			c.log.Debug("closing ContainerExecutor.Logs out log")
   211  			close(out)
   212  		}()
   213  
   214  		execution, err := c.repository.Get(ctx, id)
   215  		if err != nil {
   216  			out <- output.NewOutputError(err)
   217  			return
   218  		}
   219  
   220  		exec, err := c.executorsClient.GetByType(execution.TestType)
   221  		if err != nil {
   222  			out <- output.NewOutputError(err)
   223  			return
   224  		}
   225  
   226  		supportArtifacts := false
   227  		for _, feature := range exec.Spec.Features {
   228  			if feature == executorv1.FeatureArtifacts {
   229  				supportArtifacts = true
   230  				break
   231  			}
   232  		}
   233  
   234  		ids := []string{id}
   235  		if supportArtifacts && execution.ArtifactRequest != nil &&
   236  			(execution.ArtifactRequest.StorageClassName != "" || execution.ArtifactRequest.UseDefaultStorageClassName) {
   237  			ids = append(ids, id+ScraperPodSuffix)
   238  		}
   239  
   240  		for _, podName := range ids {
   241  			logs := make(chan []byte, logsChannelBuffer)
   242  
   243  			if err := c.TailJobLogs(ctx, podName, namespace, logs); err != nil {
   244  				out <- output.NewOutputError(err)
   245  				return
   246  			}
   247  
   248  			for l := range logs {
   249  				entry := output.NewOutputLine(l)
   250  				out <- entry
   251  				c.log.Debugw("passing log line output", "line", string(l))
   252  			}
   253  		}
   254  	}()
   255  
   256  	return
   257  }
   258  
   259  // Execute starts new external test execution, reads data and returns ID
   260  // Execution is started asynchronously client can check later for results
   261  func (c *ContainerExecutor) Execute(ctx context.Context, execution *testkube.Execution, options client.ExecuteOptions) (*testkube.ExecutionResult, error) {
   262  	executionResult := testkube.NewRunningExecutionResult()
   263  	execution.ExecutionResult = executionResult
   264  
   265  	jobOptions, err := c.createJob(ctx, *execution, options)
   266  	if err != nil {
   267  		executionResult.Err(err)
   268  		if cErr := c.cleanPVCVolume(ctx, execution); cErr != nil {
   269  			c.log.Errorw("error cleaning pvc volume", "error", cErr)
   270  		}
   271  
   272  		return executionResult, err
   273  	}
   274  
   275  	podsClient := c.clientSet.CoreV1().Pods(execution.TestNamespace)
   276  	pods, err := executor.GetJobPods(ctx, podsClient, execution.Id, 1, 10)
   277  	if err != nil {
   278  		executionResult.Err(err)
   279  		if cErr := c.cleanPVCVolume(ctx, execution); cErr != nil {
   280  			c.log.Errorw("error cleaning pvc volume", "error", cErr)
   281  		}
   282  
   283  		return executionResult, err
   284  	}
   285  
   286  	l := c.log.With("executionID", execution.Id, "sync", options.Sync)
   287  
   288  	for _, pod := range pods.Items {
   289  		if pod.Status.Phase != corev1.PodRunning && pod.Labels["job-name"] == execution.Id {
   290  			if options.Sync {
   291  				return c.updateResultsFromPod(ctx, pod, l, execution, jobOptions, options.Request.NegativeTest)
   292  			}
   293  
   294  			// async wait for complete status or error
   295  			go func(pod corev1.Pod) {
   296  				_, err := c.updateResultsFromPod(ctx, pod, l, execution, jobOptions, options.Request.NegativeTest)
   297  				if err != nil {
   298  					l.Errorw("update results from jobs pod error", "error", err)
   299  				}
   300  			}(pod)
   301  
   302  			return executionResult, nil
   303  		}
   304  	}
   305  
   306  	l.Debugw("no pods was found", "totalPodsCount", len(pods.Items))
   307  
   308  	return execution.ExecutionResult, nil
   309  }
   310  
   311  // createJob creates new Kubernetes job based on execution and execute options
   312  func (c *ContainerExecutor) createJob(ctx context.Context, execution testkube.Execution, options client.ExecuteOptions) (*JobOptions, error) {
   313  	jobsClient := c.clientSet.BatchV1().Jobs(execution.TestNamespace)
   314  
   315  	// Fallback to one-time inspector when non-default namespace is needed
   316  	inspector := c.imageInspector
   317  	if len(options.ImagePullSecretNames) > 0 && options.Namespace != "" && execution.TestNamespace != options.Namespace {
   318  		secretClient, err := secret.NewClient(options.Namespace)
   319  		if err != nil {
   320  			return nil, errors.Wrap(err, "failed to build secrets client")
   321  		}
   322  		inspector = imageinspector.NewInspector(c.registry, imageinspector.NewSkopeoFetcher(), imageinspector.NewSecretFetcher(secretClient))
   323  	}
   324  
   325  	jobOptions, err := NewJobOptions(c.log, c.templatesClient, c.images, c.templates, inspector,
   326  		c.serviceAccountNames, c.registry, c.clusterID, c.apiURI, execution, options, c.natsURI, c.debug)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	if jobOptions.ArtifactRequest != nil &&
   332  		(jobOptions.ArtifactRequest.StorageClassName != "" || jobOptions.ArtifactRequest.UseDefaultStorageClassName) {
   333  		c.log.Debug("creating persistent volume claim with options", "options", jobOptions)
   334  		pvcsClient := c.clientSet.CoreV1().PersistentVolumeClaims(execution.TestNamespace)
   335  		pvcSpec, err := client.NewPersistentVolumeClaimSpec(c.log, NewPVCOptionsFromJobOptions(*jobOptions, c.defaultStorageClassName))
   336  		if err != nil {
   337  			return nil, err
   338  		}
   339  
   340  		_, err = pvcsClient.Create(ctx, pvcSpec, metav1.CreateOptions{})
   341  		if err != nil {
   342  			return nil, err
   343  		}
   344  	}
   345  
   346  	c.log.Debug("creating executor job with options", "options", jobOptions)
   347  	jobSpec, err := NewExecutorJobSpec(c.log, jobOptions)
   348  	if err != nil {
   349  		return nil, err
   350  	}
   351  
   352  	_, err = jobsClient.Create(ctx, jobSpec, metav1.CreateOptions{})
   353  	return jobOptions, err
   354  }
   355  
   356  func (c *ContainerExecutor) cleanPVCVolume(ctx context.Context, execution *testkube.Execution) error {
   357  	if execution.ArtifactRequest != nil &&
   358  		(execution.ArtifactRequest.StorageClassName != "" || execution.ArtifactRequest.UseDefaultStorageClassName) {
   359  		pvcsClient := c.clientSet.CoreV1().PersistentVolumeClaims(execution.TestNamespace)
   360  		if err := pvcsClient.Delete(ctx, execution.Id+"-pvc", metav1.DeleteOptions{}); err != nil {
   361  			return err
   362  		}
   363  	}
   364  
   365  	return nil
   366  }
   367  
   368  // updateResultsFromPod watches logs and stores results if execution is finished
   369  func (c *ContainerExecutor) updateResultsFromPod(
   370  	ctx context.Context,
   371  	executorPod corev1.Pod,
   372  	l *zap.SugaredLogger,
   373  	execution *testkube.Execution,
   374  	jobOptions *JobOptions,
   375  	isNegativeTest bool,
   376  ) (*testkube.ExecutionResult, error) {
   377  	// save stop time and final state
   378  	defer func() {
   379  		c.stopExecution(ctx, execution, execution.ExecutionResult, isNegativeTest)
   380  
   381  		if err := c.cleanPVCVolume(ctx, execution); err != nil {
   382  			l.Errorw("error cleaning pvc volume", "error", err)
   383  		}
   384  	}()
   385  
   386  	// wait for pod
   387  	l.Debug("poll immediate waiting for executor pod")
   388  
   389  	var err error
   390  	if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, executorPod.Name, execution.TestNamespace)); err != nil {
   391  		l.Errorw("waiting for executor pod started error", "error", err)
   392  	} else if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.clientSet, executorPod.Name, execution.TestNamespace)); err != nil {
   393  		// continue on poll err and try to get logs later
   394  		l.Errorw("waiting for executor pod complete error", "error", err)
   395  	}
   396  	if err != nil {
   397  		execution.ExecutionResult.Err(err)
   398  	}
   399  	l.Debug("poll executor immediate end")
   400  
   401  	// we need to retrieve the Pod to get its latest status
   402  	podsClient := c.clientSet.CoreV1().Pods(execution.TestNamespace)
   403  	latestExecutorPod, err := podsClient.Get(context.Background(), executorPod.Name, metav1.GetOptions{})
   404  	if err != nil {
   405  		execution.ExecutionResult.Err(err)
   406  		return execution.ExecutionResult, nil
   407  	}
   408  
   409  	var scraperLogs []byte
   410  	if jobOptions.ArtifactRequest != nil &&
   411  		(jobOptions.ArtifactRequest.StorageClassName != "" || execution.ArtifactRequest.UseDefaultStorageClassName) {
   412  		c.log.Debug("creating scraper job with options", "options", jobOptions)
   413  		jobsClient := c.clientSet.BatchV1().Jobs(execution.TestNamespace)
   414  		scraperSpec, err := NewScraperJobSpec(c.log, jobOptions)
   415  		if err != nil {
   416  			return execution.ExecutionResult, err
   417  		}
   418  
   419  		_, err = jobsClient.Create(ctx, scraperSpec, metav1.CreateOptions{})
   420  		if err != nil {
   421  			return execution.ExecutionResult, err
   422  		}
   423  
   424  		scraperPodName := execution.Id + ScraperPodSuffix
   425  		scraperPods, err := executor.GetJobPods(ctx, podsClient, scraperPodName, 1, 10)
   426  		if err != nil {
   427  			return execution.ExecutionResult, err
   428  		}
   429  
   430  		// get scraper job pod and
   431  		for _, scraperPod := range scraperPods.Items {
   432  			if scraperPod.Status.Phase != corev1.PodRunning && scraperPod.Labels["job-name"] == scraperPodName {
   433  				l.Debug("poll immediate waiting for scraper pod to succeed")
   434  				if err = wait.PollUntilContextTimeout(ctx, pollInterval, c.podStartTimeout, true, executor.IsPodLoggable(c.clientSet, scraperPod.Name, execution.TestNamespace)); err != nil {
   435  					l.Errorw("waiting for scraper pod started error", "error", err)
   436  				} else if err = wait.PollUntilContextTimeout(ctx, pollInterval, pollTimeout, true, executor.IsPodReady(c.clientSet, scraperPod.Name, execution.TestNamespace)); err != nil {
   437  					// continue on poll err and try to get logs later
   438  					l.Errorw("waiting for scraper pod complete error", "error", err)
   439  				}
   440  				if err != nil {
   441  					execution.ExecutionResult.Err(err)
   442  				}
   443  				l.Debug("poll scraper immediate end")
   444  
   445  				latestScraperPod, err := podsClient.Get(context.Background(), scraperPod.Name, metav1.GetOptions{})
   446  				if err != nil {
   447  					execution.ExecutionResult.Err(err)
   448  					return execution.ExecutionResult, nil
   449  				}
   450  
   451  				switch latestScraperPod.Status.Phase {
   452  				case corev1.PodSucceeded:
   453  					execution.ExecutionResult.Success()
   454  				case corev1.PodFailed:
   455  					execution.ExecutionResult.Error()
   456  				}
   457  
   458  				scraperLogs, err = executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestScraperPod)
   459  				if err != nil {
   460  					l.Errorw("get scraper pod logs error", "error", err)
   461  				}
   462  
   463  				break
   464  			}
   465  		}
   466  	}
   467  
   468  	if !execution.ExecutionResult.IsFailed() {
   469  		switch latestExecutorPod.Status.Phase {
   470  		case corev1.PodSucceeded:
   471  			execution.ExecutionResult.Success()
   472  		case corev1.PodFailed:
   473  			execution.ExecutionResult.Error()
   474  		}
   475  	}
   476  
   477  	executorLogs, err := executor.GetPodLogs(ctx, c.clientSet, execution.TestNamespace, *latestExecutorPod)
   478  	if err != nil {
   479  		l.Errorw("get executor pod logs error", "error", err)
   480  	}
   481  
   482  	executorLogs = append(executorLogs, scraperLogs...)
   483  	if len(executorLogs) != 0 {
   484  		// parse container output log (mixed JSON and plain text stream)
   485  		executionResult, output, err := output.ParseContainerOutput(executorLogs)
   486  		if err != nil {
   487  			l.Errorw("parse output error", "error", err)
   488  			execution.ExecutionResult.Output = output
   489  			execution.ExecutionResult.Err(err)
   490  			err = c.repository.UpdateResult(ctx, execution.Id, *execution)
   491  			if err != nil {
   492  				l.Infow("Update result", "error", err)
   493  			}
   494  			return execution.ExecutionResult, err
   495  		}
   496  
   497  		if executionResult != nil {
   498  			execution.ExecutionResult = executionResult
   499  		}
   500  
   501  		// don't attach logs if logs v2 is enabled - they will be streamed through the logs service
   502  		attachLogs := !c.features.LogsV2
   503  		if attachLogs {
   504  			execution.ExecutionResult.Output = output
   505  		}
   506  	}
   507  
   508  	if execution.ExecutionResult.IsFailed() {
   509  		errorMessage := execution.ExecutionResult.ErrorMessage
   510  		if errorMessage == "" {
   511  			errorMessage = executor.GetPodErrorMessage(ctx, c.clientSet, latestExecutorPod)
   512  		}
   513  
   514  		execution.ExecutionResult.ErrorMessage = errorMessage
   515  	}
   516  
   517  	l.Infow("container execution completed saving result", "executionId", execution.Id, "status", execution.ExecutionResult.Status)
   518  	err = c.repository.UpdateResult(ctx, execution.Id, *execution)
   519  	if err != nil {
   520  		l.Errorw("Update execution result error", "error", err)
   521  	}
   522  	return execution.ExecutionResult, nil
   523  }
   524  
   525  func (c *ContainerExecutor) stopExecution(ctx context.Context,
   526  	execution *testkube.Execution,
   527  	result *testkube.ExecutionResult,
   528  	isNegativeTest bool,
   529  ) {
   530  	c.log.Debugw("stopping execution", "isNegativeTest", isNegativeTest, "test", execution.TestName)
   531  	execution.Stop()
   532  
   533  	if isNegativeTest {
   534  		if result.IsFailed() {
   535  			c.log.Debugw("test run was expected to fail, and it failed as expected", "test", execution.TestName)
   536  			execution.ExecutionResult.Status = testkube.ExecutionStatusPassed
   537  			execution.ExecutionResult.ErrorMessage = ""
   538  			result.Output = result.Output + "\nTest run was expected to fail, and it failed as expected"
   539  		} else {
   540  			c.log.Debugw("test run was expected to fail - the result will be reversed", "test", execution.TestName)
   541  			execution.ExecutionResult.Status = testkube.ExecutionStatusFailed
   542  			execution.ExecutionResult.ErrorMessage = "negative test error"
   543  			result.Output = result.Output + "\nTest run was expected to fail, the result will be reversed"
   544  		}
   545  
   546  		result.Status = execution.ExecutionResult.Status
   547  		result.ErrorMessage = execution.ExecutionResult.ErrorMessage
   548  		err := c.repository.UpdateResult(ctx, execution.Id, *execution)
   549  		if err != nil {
   550  			c.log.Errorw("Update execution result error", "error", err)
   551  		}
   552  	}
   553  
   554  	err := c.repository.EndExecution(ctx, *execution)
   555  	if err != nil {
   556  		c.log.Errorw("Update execution result error", "error", err)
   557  	}
   558  
   559  	// metrics increase
   560  	execution.ExecutionResult = result
   561  	c.metrics.IncAndObserveExecuteTest(*execution, c.dashboardURI)
   562  
   563  	test, err := c.testsClient.Get(execution.TestName)
   564  	if err != nil {
   565  		c.log.Errorw("getting test error", "error", err)
   566  	}
   567  
   568  	if test != nil {
   569  		test.Status = testsmapper.MapExecutionToTestStatus(execution)
   570  		if err = c.testsClient.UpdateStatus(test); err != nil {
   571  			c.log.Errorw("updating test error", "error", err)
   572  		}
   573  	}
   574  
   575  	if execution.TestExecutionName != "" {
   576  		testExecution, err := c.testExecutionsClient.Get(execution.TestExecutionName)
   577  		if err != nil {
   578  			c.log.Errorw("getting test execution error", "error", err)
   579  		}
   580  
   581  		if testExecution != nil {
   582  			testExecution.Status = testexecutionsmapper.MapAPIToCRD(execution, testExecution.Generation)
   583  			if err = c.testExecutionsClient.UpdateStatus(testExecution); err != nil {
   584  				c.log.Errorw("updating test execution error", "error", err)
   585  			}
   586  		}
   587  	}
   588  
   589  	if result.IsPassed() {
   590  		c.emitter.Notify(testkube.NewEventEndTestSuccess(execution))
   591  	} else if result.IsTimeout() {
   592  		c.emitter.Notify(testkube.NewEventEndTestTimeout(execution))
   593  	} else if result.IsAborted() {
   594  		c.emitter.Notify(testkube.NewEventEndTestAborted(execution))
   595  	} else {
   596  		c.emitter.Notify(testkube.NewEventEndTestFailed(execution))
   597  	}
   598  
   599  	telemetryEnabled, err := c.configMap.GetTelemetryEnabled(ctx)
   600  	if err != nil {
   601  		c.log.Debugw("getting telemetry enabled error", "error", err)
   602  	}
   603  
   604  	if !telemetryEnabled {
   605  		return
   606  	}
   607  
   608  	clusterID, err := c.configMap.GetUniqueClusterId(ctx)
   609  	if err != nil {
   610  		c.log.Debugw("getting cluster id error", "error", err)
   611  	}
   612  
   613  	host, err := os.Hostname()
   614  	if err != nil {
   615  		c.log.Debugw("getting hostname error", "hostname", host, "error", err)
   616  	}
   617  
   618  	var dataSource string
   619  	if execution.Content != nil {
   620  		dataSource = execution.Content.Type_
   621  	}
   622  
   623  	status := ""
   624  	if execution.ExecutionResult != nil && execution.ExecutionResult.Status != nil {
   625  		status = string(*execution.ExecutionResult.Status)
   626  	}
   627  
   628  	out, err := telemetry.SendRunEvent("testkube_api_run_test", telemetry.RunParams{
   629  		AppVersion: version.Version,
   630  		DataSource: dataSource,
   631  		Host:       host,
   632  		ClusterID:  clusterID,
   633  		TestType:   execution.TestType,
   634  		DurationMs: execution.DurationMs,
   635  		Status:     status,
   636  	})
   637  	if err != nil {
   638  		c.log.Debugw("sending run test telemetry event error", "error", err)
   639  	} else {
   640  		c.log.Debugw("sending run test telemetry event", "output", out)
   641  	}
   642  
   643  }
   644  
   645  // NewJobOptionsFromExecutionOptions compose JobOptions based on ExecuteOptions
   646  func NewJobOptionsFromExecutionOptions(options client.ExecuteOptions) *JobOptions {
   647  	// for image, HTTP request takes priority, then test spec, then executor
   648  	var image string
   649  	if options.ExecutorSpec.Image != "" {
   650  		image = options.ExecutorSpec.Image
   651  	}
   652  
   653  	if options.TestSpec.ExecutionRequest != nil &&
   654  		options.TestSpec.ExecutionRequest.Image != "" {
   655  		image = options.TestSpec.ExecutionRequest.Image
   656  	}
   657  
   658  	if options.Request.Image != "" {
   659  		image = options.Request.Image
   660  	}
   661  
   662  	var workingDir string
   663  	if options.TestSpec.Content != nil &&
   664  		options.TestSpec.Content.Repository != nil &&
   665  		options.TestSpec.Content.Repository.WorkingDir != "" {
   666  		workingDir = options.TestSpec.Content.Repository.WorkingDir
   667  		if !filepath.IsAbs(workingDir) {
   668  			workingDir = filepath.Join(repoPath, workingDir)
   669  		}
   670  	}
   671  
   672  	supportArtifacts := false
   673  	for _, feature := range options.ExecutorSpec.Features {
   674  		if feature == executorv1.FeatureArtifacts {
   675  			supportArtifacts = true
   676  			break
   677  		}
   678  	}
   679  
   680  	var artifactRequest *testkube.ArtifactRequest
   681  	jobDelaySeconds := jobDefaultDelaySeconds
   682  	if supportArtifacts {
   683  		artifactRequest = options.Request.ArtifactRequest
   684  		jobDelaySeconds = jobArtifactDelaySeconds
   685  	}
   686  
   687  	labels := map[string]string{
   688  		testkube.TestLabelTestType: utils.SanitizeName(options.TestSpec.Type_),
   689  		testkube.TestLabelExecutor: options.ExecutorName,
   690  		testkube.TestLabelTestName: options.TestName,
   691  	}
   692  	for key, value := range options.Labels {
   693  		labels[key] = value
   694  	}
   695  
   696  	contextType := ""
   697  	contextData := ""
   698  	if options.Request.RunningContext != nil {
   699  		contextType = options.Request.RunningContext.Type_
   700  		contextData = options.Request.RunningContext.Context
   701  	}
   702  
   703  	return &JobOptions{
   704  		Image:                     image,
   705  		ImagePullSecrets:          options.ImagePullSecretNames,
   706  		Args:                      options.Request.Args,
   707  		Command:                   options.Request.Command,
   708  		WorkingDir:                workingDir,
   709  		TestName:                  options.TestName,
   710  		Namespace:                 options.Namespace,
   711  		Envs:                      options.Request.Envs,
   712  		SecretEnvs:                options.Request.SecretEnvs,
   713  		HTTPProxy:                 options.Request.HttpProxy,
   714  		HTTPSProxy:                options.Request.HttpsProxy,
   715  		UsernameSecret:            options.UsernameSecret,
   716  		TokenSecret:               options.TokenSecret,
   717  		RunnerCustomCASecret:      options.RunnerCustomCASecret,
   718  		CertificateSecret:         options.CertificateSecret,
   719  		AgentAPITLSSecret:         options.AgentAPITLSSecret,
   720  		ActiveDeadlineSeconds:     options.Request.ActiveDeadlineSeconds,
   721  		ArtifactRequest:           artifactRequest,
   722  		DelaySeconds:              jobDelaySeconds,
   723  		JobTemplate:               options.ExecutorSpec.JobTemplate,
   724  		JobTemplateExtensions:     options.Request.JobTemplate,
   725  		ScraperTemplateExtensions: options.Request.ScraperTemplate,
   726  		PvcTemplateExtensions:     options.Request.PvcTemplate,
   727  		EnvConfigMaps:             options.Request.EnvConfigMaps,
   728  		EnvSecrets:                options.Request.EnvSecrets,
   729  		Labels:                    labels,
   730  		ExecutionNumber:           options.Request.Number,
   731  		ContextType:               contextType,
   732  		ContextData:               contextData,
   733  		Features:                  options.Features,
   734  	}
   735  }
   736  
   737  // Abort K8sJob aborts K8S by job name
   738  func (c *ContainerExecutor) Abort(ctx context.Context, execution *testkube.Execution) (*testkube.ExecutionResult, error) {
   739  	return executor.AbortJob(ctx, c.clientSet, execution.TestNamespace, execution.Id)
   740  }
   741  
   742  func NewPVCOptionsFromJobOptions(options JobOptions, defaultStorageClassName string) client.PVCOptions {
   743  	result := client.PVCOptions{
   744  		Name:                    options.Name,
   745  		Namespace:               options.Namespace,
   746  		PvcTemplate:             options.PvcTemplate,
   747  		PvcTemplateExtensions:   options.PvcTemplateExtensions,
   748  		ArtifactRequest:         options.ArtifactRequest,
   749  		DefaultStorageClassName: defaultStorageClassName,
   750  	}
   751  
   752  	return result
   753  }