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(©Files, "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 }