github.com/kubeshop/testkube@v1.17.23/cmd/kubectl-testkube/commands/tests/run.go (about)

     1  package tests
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/google/uuid"
    11  	"github.com/pkg/errors"
    12  	"github.com/spf13/cobra"
    13  
    14  	"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
    15  	"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render"
    16  	apiv1 "github.com/kubeshop/testkube/pkg/api/v1/client"
    17  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    18  	"github.com/kubeshop/testkube/pkg/ui"
    19  )
    20  
    21  func NewRunTestCmd() *cobra.Command {
    22  	var (
    23  		name                               string
    24  		image                              string
    25  		iterations                         int
    26  		watchEnabled                       bool
    27  		executorArgs                       []string
    28  		variables                          []string
    29  		secretVariables                    []string
    30  		variablesFile                      string
    31  		downloadArtifactsEnabled           bool
    32  		downloadDir                        string
    33  		envs                               map[string]string
    34  		secretEnvs                         map[string]string
    35  		selectors                          []string
    36  		concurrencyLevel                   int
    37  		httpProxy, httpsProxy              string
    38  		executionLabels                    map[string]string
    39  		secretVariableReferences           map[string]string
    40  		copyFiles                          []string
    41  		artifactStorageClassName           string
    42  		artifactVolumeMountPath            string
    43  		artifactDirs                       []string
    44  		artifactMasks                      []string
    45  		jobTemplate                        string
    46  		jobTemplateReference               string
    47  		gitBranch                          string
    48  		gitCommit                          string
    49  		gitPath                            string
    50  		gitWorkingDir                      string
    51  		preRunScript                       string
    52  		postRunScript                      string
    53  		executePostRunScriptBeforeScraping bool
    54  		sourceScripts                      bool
    55  		scraperTemplate                    string
    56  		scraperTemplateReference           string
    57  		pvcTemplate                        string
    58  		pvcTemplateReference               string
    59  		negativeTest                       bool
    60  		mountConfigMaps                    map[string]string
    61  		variableConfigMaps                 []string
    62  		mountSecrets                       map[string]string
    63  		variableSecrets                    []string
    64  		uploadTimeout                      string
    65  		format                             string
    66  		masks                              []string
    67  		runningContext                     string
    68  		command                            []string
    69  		argsMode                           string
    70  		artifactStorageBucket              string
    71  		artifactOmitFolderPerExecution     bool
    72  		artifactSharedBetweenPods          bool
    73  		artifactUseDefaultStorageClassName bool
    74  		silentMode                         bool
    75  		slavePodRequestsCpu                string
    76  		slavePodRequestsMemory             string
    77  		slavePodLimitsCpu                  string
    78  		slavePodLimitsMemory               string
    79  		slavePodTemplate                   string
    80  		slavePodTemplateReference          string
    81  		executionNamespace                 string
    82  	)
    83  
    84  	cmd := &cobra.Command{
    85  		Use:     "test <testName>",
    86  		Aliases: []string{"t"},
    87  		Short:   "Starts new test",
    88  		Long:    `Starts new test based on Test Custom Resource name, returns results to console`,
    89  		Run: func(cmd *cobra.Command, args []string) {
    90  			envs, err := cmd.Flags().GetStringToString("env")
    91  			ui.WarnOnError("getting envs", err)
    92  
    93  			client, _, err := common.GetClient(cmd)
    94  			ui.ExitOnError("getting client", err)
    95  
    96  			info, err := client.GetServerInfo()
    97  			ui.ExitOnError("getting server info", err)
    98  
    99  			variables, err := common.CreateVariables(cmd, info.DisableSecretCreation)
   100  			ui.WarnOnError("getting variables", err)
   101  
   102  			envConfigMaps, envSecrets, err := newEnvReferencesFromFlags(cmd)
   103  			ui.WarnOnError("getting env config maps and secrets", err)
   104  
   105  			mode := ""
   106  			if cmd.Flag("args-mode").Changed {
   107  				mode = argsMode
   108  			}
   109  
   110  			options := apiv1.ExecuteTestOptions{
   111  				ExecutionVariables:         variables,
   112  				ExecutionLabels:            executionLabels,
   113  				Command:                    command,
   114  				Args:                       executorArgs,
   115  				ArgsMode:                   mode,
   116  				SecretEnvs:                 secretEnvs,
   117  				HTTPProxy:                  httpProxy,
   118  				HTTPSProxy:                 httpsProxy,
   119  				Envs:                       envs,
   120  				Image:                      image,
   121  				JobTemplateReference:       jobTemplateReference,
   122  				ScraperTemplateReference:   scraperTemplateReference,
   123  				PvcTemplateReference:       pvcTemplateReference,
   124  				IsNegativeTestChangedOnRun: false,
   125  				EnvConfigMaps:              envConfigMaps,
   126  				EnvSecrets:                 envSecrets,
   127  				RunningContext: &testkube.RunningContext{
   128  					Type_:   string(testkube.RunningContextTypeUserCLI),
   129  					Context: runningContext,
   130  				},
   131  				ExecutePostRunScriptBeforeScraping: executePostRunScriptBeforeScraping,
   132  				SourceScripts:                      sourceScripts,
   133  				ExecutionNamespace:                 executionNamespace,
   134  			}
   135  
   136  			var fields = []struct {
   137  				source      string
   138  				title       string
   139  				destination *string
   140  			}{
   141  				{
   142  					jobTemplate,
   143  					"job template",
   144  					&options.JobTemplate,
   145  				},
   146  				{
   147  					preRunScript,
   148  					"pre run script",
   149  					&options.PreRunScriptContent,
   150  				},
   151  				{
   152  					postRunScript,
   153  					"post run script",
   154  					&options.PostRunScriptContent,
   155  				},
   156  				{
   157  					scraperTemplate,
   158  					"scraper template",
   159  					&options.ScraperTemplate,
   160  				},
   161  				{
   162  					pvcTemplate,
   163  					"pvc template",
   164  					&options.PvcTemplate,
   165  				},
   166  			}
   167  
   168  			for _, field := range fields {
   169  				if field.source != "" {
   170  					b, err := os.ReadFile(field.source)
   171  					ui.ExitOnError("reading "+field.title, err)
   172  					*field.destination = string(b)
   173  				}
   174  			}
   175  
   176  			var executions []testkube.Execution
   177  			client, namespace, err := common.GetClient(cmd)
   178  			ui.ExitOnError("getting client", err)
   179  
   180  			if artifactStorageClassName != "" || artifactVolumeMountPath != "" || len(artifactDirs) != 0 ||
   181  				artifactStorageBucket != "" || artifactOmitFolderPerExecution || artifactUseDefaultStorageClassName {
   182  				options.ArtifactRequest = &testkube.ArtifactRequest{
   183  					StorageClassName:           artifactStorageClassName,
   184  					VolumeMountPath:            artifactVolumeMountPath,
   185  					Dirs:                       artifactDirs,
   186  					StorageBucket:              artifactStorageBucket,
   187  					OmitFolderPerExecution:     artifactOmitFolderPerExecution,
   188  					UseDefaultStorageClassName: artifactUseDefaultStorageClassName,
   189  				}
   190  			}
   191  
   192  			if cmd.Flag("negative-test").Changed {
   193  				options.NegativeTest = negativeTest
   194  				options.IsNegativeTestChangedOnRun = true
   195  			}
   196  
   197  			if gitBranch != "" || gitCommit != "" || gitPath != "" || gitWorkingDir != "" {
   198  				options.ContentRequest = &testkube.TestContentRequest{
   199  					Repository: &testkube.RepositoryParameters{
   200  						Branch:     gitBranch,
   201  						Commit:     gitCommit,
   202  						Path:       gitPath,
   203  						WorkingDir: gitWorkingDir,
   204  					},
   205  				}
   206  			}
   207  
   208  			if slavePodRequestsCpu != "" || slavePodRequestsMemory != "" || slavePodLimitsCpu != "" ||
   209  				slavePodLimitsMemory != "" || slavePodTemplate != "" || slavePodTemplateReference != "" {
   210  				options.SlavePodRequest = &testkube.PodRequest{
   211  					PodTemplateReference: slavePodTemplateReference,
   212  				}
   213  
   214  				if slavePodTemplate != "" {
   215  					b, err := os.ReadFile(slavePodTemplate)
   216  					ui.ExitOnError("reading slave pod template", err)
   217  					options.SlavePodRequest.PodTemplate = string(b)
   218  				}
   219  
   220  				if slavePodRequestsCpu != "" || slavePodRequestsMemory != "" {
   221  					if options.SlavePodRequest.Resources == nil {
   222  						options.SlavePodRequest.Resources = &testkube.PodResourcesRequest{}
   223  					}
   224  
   225  					options.SlavePodRequest.Resources.Requests = &testkube.ResourceRequest{
   226  						Cpu:    slavePodRequestsCpu,
   227  						Memory: slavePodRequestsMemory,
   228  					}
   229  				}
   230  
   231  				if slavePodLimitsCpu != "" || slavePodLimitsMemory != "" {
   232  					if options.SlavePodRequest.Resources == nil {
   233  						options.SlavePodRequest.Resources = &testkube.PodResourcesRequest{}
   234  					}
   235  
   236  					options.SlavePodRequest.Resources.Limits = &testkube.ResourceRequest{
   237  						Cpu:    slavePodLimitsCpu,
   238  						Memory: slavePodLimitsMemory,
   239  					}
   240  				}
   241  			}
   242  
   243  			switch {
   244  			case len(args) > 0:
   245  				testName := args[0]
   246  				namespacedName := fmt.Sprintf("%s/%s", namespace, testName)
   247  
   248  				test, err := client.GetTest(testName)
   249  				if err != nil {
   250  					ui.UseStderr()
   251  					ui.Errf("Can't get test with name '%s'. Test does not exist in namespace '%s'", testName, namespace)
   252  					ui.Debug(err.Error())
   253  					os.Exit(1)
   254  				}
   255  
   256  				var timeout time.Duration
   257  				if uploadTimeout != "" {
   258  					timeout, err = time.ParseDuration(uploadTimeout)
   259  					if err != nil {
   260  						ui.ExitOnError("invalid upload timeout duration", err)
   261  					}
   262  				}
   263  
   264  				options.BucketName = uuid.New().String()
   265  				if len(variablesFile) > 0 {
   266  					options.ExecutionVariablesFileContent, options.IsVariablesFileUploaded, err = PrepareVariablesFile(client, options.BucketName, apiv1.Execution, variablesFile, timeout)
   267  					if err != nil {
   268  						ui.ExitOnError("could not prepare variables file", err)
   269  					}
   270  				}
   271  
   272  				if len(copyFiles) > 0 {
   273  					err = uploadFiles(client, options.BucketName, apiv1.Execution, copyFiles, timeout)
   274  					ui.ExitOnError("could not upload files", err)
   275  				}
   276  
   277  				if len(test.Uploads) != 0 || len(copyFiles) != 0 {
   278  					copyFileList, err := mergeCopyFiles(test.Uploads, copyFiles)
   279  					ui.ExitOnError("could not merge files", err)
   280  
   281  					ui.Warn("Testkube will use the following file mappings:", copyFileList...)
   282  				}
   283  
   284  				for i := 0; i < iterations; i++ {
   285  					execution, err := client.ExecuteTest(testName, name, options)
   286  					ui.ExitOnError("starting test execution "+namespacedName, err)
   287  					executions = append(executions, execution)
   288  				}
   289  			case len(selectors) != 0:
   290  				selector := strings.Join(selectors, ",")
   291  				executions, err = client.ExecuteTests(selector, concurrencyLevel, options)
   292  				ui.ExitOnError("starting test executions "+selector, err)
   293  			default:
   294  				ui.Failf("Pass Test name or labels to run by labels ")
   295  			}
   296  
   297  			go func() {
   298  				<-cmd.Context().Done()
   299  				if errors.Is(cmd.Context().Err(), context.Canceled) {
   300  					os.Exit(0)
   301  				}
   302  			}()
   303  
   304  			var execErrors []error
   305  			for _, execution := range executions {
   306  				printExecutionDetails(execution)
   307  
   308  				if execution.ExecutionResult != nil && execution.ExecutionResult.ErrorMessage != "" {
   309  					execErrors = append(execErrors, errors.New(execution.ExecutionResult.ErrorMessage))
   310  				}
   311  
   312  				if execution.Id != "" {
   313  					if watchEnabled && len(args) > 0 {
   314  						info, err := client.GetServerInfo()
   315  						ui.ExitOnError("getting server info", err)
   316  
   317  						if info.Features != nil && info.Features.LogsV2 {
   318  							if err = watchLogsV2(execution.Id, silentMode, client); err != nil {
   319  								execErrors = append(execErrors, err)
   320  							}
   321  						} else {
   322  							if err = watchLogs(execution.Id, silentMode, client); err != nil {
   323  								execErrors = append(execErrors, err)
   324  							}
   325  						}
   326  					}
   327  
   328  					execution, err = client.GetExecution(execution.Id)
   329  					ui.ExitOnError("getting recent execution data id:"+execution.Id, err)
   330  				}
   331  
   332  				if err = render.RenderExecutionResult(client, &execution, false, !watchEnabled); err != nil {
   333  					execErrors = append(execErrors, err)
   334  				}
   335  
   336  				if execution.Id != "" {
   337  					if watchEnabled && len(args) > 0 {
   338  						if downloadArtifactsEnabled && (execution.IsPassed() || execution.IsFailed()) {
   339  							DownloadTestArtifacts(execution.Id, downloadDir, format, masks, client)
   340  						}
   341  					}
   342  
   343  					uiShellWatchExecution(execution.Name)
   344  				}
   345  
   346  				uiShellGetExecution(execution.Name)
   347  			}
   348  
   349  			ui.ExitOnError("executions contain failed on errors", execErrors...)
   350  		},
   351  	}
   352  
   353  	cmd.Flags().StringVarP(&name, "name", "n", "", "execution name, if empty will be autogenerated")
   354  	cmd.Flags().StringVarP(&image, "image", "", "", "override executor container image")
   355  	cmd.Flags().StringVarP(&variablesFile, "variables-file", "", "", "variables file path, e.g. postman env file - will be passed to executor if supported")
   356  	cmd.Flags().StringArrayVarP(&variables, "variable", "v", []string{}, "execution variable passed to executor")
   357  	cmd.Flags().StringArrayVarP(&secretVariables, "secret-variable", "s", []string{}, "execution secret variable passed to executor")
   358  	cmd.Flags().StringArrayVar(&command, "command", []string{}, "command passed to image in executor")
   359  	cmd.Flags().StringArrayVarP(&executorArgs, "args", "", []string{}, "executor binary additional arguments")
   360  	cmd.Flags().StringVarP(&argsMode, "args-mode", "", "append", "usage mode for argumnets. one of append|override|replace")
   361  	cmd.Flags().BoolVarP(&watchEnabled, "watch", "f", false, "watch for changes after start")
   362  	cmd.Flags().StringVar(&downloadDir, "download-dir", "artifacts", "download dir")
   363  	cmd.Flags().BoolVarP(&downloadArtifactsEnabled, "download-artifacts", "d", false, "download artifacts automatically")
   364  	cmd.Flags().StringToStringVarP(&envs, "env", "", map[string]string{}, "envs in a form of name1=val1 passed to executor")
   365  	cmd.Flags().StringToStringVarP(&secretEnvs, "secret", "", map[string]string{}, "secret envs in a form of secret_key1=secret_name1 passed to executor")
   366  	cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
   367  	cmd.Flags().IntVar(&concurrencyLevel, "concurrency", 10, "concurrency level for multiple test execution")
   368  	cmd.Flags().IntVar(&iterations, "iterations", 1, "how many times to run the test")
   369  	cmd.Flags().StringVar(&httpProxy, "http-proxy", "", "http proxy for executor containers")
   370  	cmd.Flags().StringVar(&httpsProxy, "https-proxy", "", "https proxy for executor containers")
   371  	cmd.Flags().StringToStringVarP(&executionLabels, "execution-label", "", nil, "execution-label key value pair: --execution-label key1=value1")
   372  	cmd.Flags().StringToStringVarP(&secretVariableReferences, "secret-variable-reference", "", nil, "secret variable references in a form name1=secret_name1=secret_key1")
   373  	cmd.Flags().StringArrayVarP(&copyFiles, "copy-files", "", []string{}, "file path mappings from host to pod of form source:destination")
   374  	cmd.Flags().StringVar(&artifactStorageClassName, "artifact-storage-class-name", "", "artifact storage class name for container executor")
   375  	cmd.Flags().StringVar(&artifactVolumeMountPath, "artifact-volume-mount-path", "", "artifact volume mount path for container executor")
   376  	cmd.Flags().StringArrayVarP(&artifactDirs, "artifact-dir", "", []string{}, "artifact dirs for scraping")
   377  	cmd.Flags().StringArrayVarP(&artifactMasks, "artifact-mask", "", []string{}, "regexp to filter scraped artifacts, single or comma separated, like report/.* or .*\\.json,.*\\.js$")
   378  	cmd.Flags().StringVar(&jobTemplate, "job-template", "", "job template file path for extensions to job template")
   379  	cmd.Flags().StringVar(&jobTemplateReference, "job-template-reference", "", "reference to job template to use for the test")
   380  	cmd.Flags().StringVarP(&gitBranch, "git-branch", "", "", "if uri is git repository we can set additional branch parameter")
   381  	cmd.Flags().StringVarP(&gitCommit, "git-commit", "", "", "if uri is git repository we can use commit id (sha) parameter")
   382  	cmd.Flags().StringVarP(&gitPath, "git-path", "", "", "if repository is big we need to define additional path to directory/file to checkout partially")
   383  	cmd.Flags().StringVarP(&gitWorkingDir, "git-working-dir", "", "", "if repository contains multiple directories with tests (like monorepo) and one starting directory we can set working directory parameter")
   384  	cmd.Flags().StringVarP(&preRunScript, "prerun-script", "", "", "path to script to be run before test execution")
   385  	cmd.Flags().StringVarP(&postRunScript, "postrun-script", "", "", "path to script to be run after test execution")
   386  	cmd.Flags().BoolVarP(&executePostRunScriptBeforeScraping, "execute-postrun-script-before-scraping", "", false, "whether to execute postrun scipt before scraping or not (prebuilt executor only)")
   387  	cmd.Flags().BoolVarP(&sourceScripts, "source-scripts", "", false, "run scripts using source command (container executor only)")
   388  	cmd.Flags().StringVar(&scraperTemplate, "scraper-template", "", "scraper template file path for extensions to scraper template")
   389  	cmd.Flags().StringVar(&scraperTemplateReference, "scraper-template-reference", "", "reference to scraper template to use for the test")
   390  	cmd.Flags().StringVar(&pvcTemplate, "pvc-template", "", "pvc template file path for extensions to pvc template")
   391  	cmd.Flags().StringVar(&pvcTemplateReference, "pvc-template-reference", "", "reference to pvc template to use for the test")
   392  	cmd.Flags().BoolVar(&negativeTest, "negative-test", false, "negative test, if enabled, makes failure an expected and correct test result. If the test fails the result will be set to success, and vice versa")
   393  	cmd.Flags().StringToStringVarP(&mountConfigMaps, "mount-configmap", "", map[string]string{}, "config map value pair for mounting it to executor pod: --mount-configmap configmap_name=configmap_mountpath")
   394  	cmd.Flags().StringArrayVar(&variableConfigMaps, "variable-configmap", []string{}, "config map name used to map all keys to basis variables")
   395  	cmd.Flags().StringToStringVarP(&mountSecrets, "mount-secret", "", map[string]string{}, "secret value pair for mounting it to executor pod: --mount-secret secret_name=secret_mountpath")
   396  	cmd.Flags().StringArrayVar(&variableSecrets, "variable-secret", []string{}, "secret name used to map all keys to secret variables")
   397  	cmd.Flags().MarkDeprecated("env", "env is deprecated use variable instead")
   398  	cmd.Flags().MarkDeprecated("secret", "secret-env is deprecated use secret-variable instead")
   399  	cmd.Flags().StringVar(&uploadTimeout, "upload-timeout", "", "timeout to use when uploading files, example: 30s")
   400  	cmd.Flags().StringVar(&format, "format", "folder", "data format for storing files, one of folder|archive")
   401  	cmd.Flags().StringArrayVarP(&masks, "mask", "", []string{}, "regexp to filter downloaded files, single or comma separated, like report/.* or .*\\.json,.*\\.js$")
   402  	cmd.Flags().StringVar(&runningContext, "context", "", "running context description for test execution")
   403  	cmd.Flags().StringVar(&artifactStorageBucket, "artifact-storage-bucket", "", "artifact storage bucket")
   404  	cmd.Flags().BoolVarP(&artifactOmitFolderPerExecution, "artifact-omit-folder-per-execution", "", false, "don't store artifacts in execution folder")
   405  	cmd.Flags().BoolVarP(&artifactSharedBetweenPods, "artifact-shared-between-pods", "", false, "whether to share volume between pods")
   406  	cmd.Flags().BoolVarP(&artifactUseDefaultStorageClassName, "artifact-use-default-storage-class-name", "", false, "whether to use default storage class name")
   407  	cmd.Flags().BoolVarP(&silentMode, "silent", "", false, "don't print intermediate test execution")
   408  	cmd.Flags().StringVar(&slavePodRequestsCpu, "slave-pod-requests-cpu", "", "slave pod resource requests cpu")
   409  	cmd.Flags().StringVar(&slavePodRequestsMemory, "slave-pod-requests-memory", "", "slave pod resource requests memory")
   410  	cmd.Flags().StringVar(&slavePodLimitsCpu, "slave-pod-limits-cpu", "", "slave pod resource limits cpu")
   411  	cmd.Flags().StringVar(&slavePodLimitsMemory, "slave-pod-limits-memory", "", "slave pod resource limits memory")
   412  	cmd.Flags().StringVar(&slavePodTemplate, "slave-pod-template", "", "slave pod template file path for extensions to slave pod template")
   413  	cmd.Flags().StringVar(&slavePodTemplateReference, "slave-pod-template-reference", "", "reference to slave pod template to use for the test")
   414  	cmd.Flags().StringVar(&executionNamespace, "execution-namespace", "", "namespace for test execution (Pro edition only)")
   415  
   416  	return cmd
   417  }
   418  
   419  func uiShellGetExecution(id string) {
   420  	ui.ShellCommand(
   421  		"Use following command to get test execution details",
   422  		"kubectl testkube get execution "+id,
   423  	)
   424  
   425  	ui.NL()
   426  }
   427  
   428  func uiShellWatchExecution(id string) {
   429  	ui.ShellCommand(
   430  		"Watch test execution until complete",
   431  		"kubectl testkube watch execution "+id,
   432  	)
   433  
   434  	ui.NL()
   435  }