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 }