github.com/kubeshop/testkube@v1.17.23/cmd/tcl/testworkflow-toolkit/commands/execute.go (about) 1 // Copyright 2024 Testkube. 2 // 3 // Licensed as a Testkube Pro file under the Testkube Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt 8 9 package commands 10 11 import ( 12 "encoding/json" 13 "fmt" 14 "os" 15 "sync" 16 "time" 17 18 "github.com/pkg/errors" 19 "github.com/spf13/cobra" 20 21 testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" 22 "github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data" 23 common2 "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/common" 24 "github.com/kubeshop/testkube/cmd/tcl/testworkflow-toolkit/env" 25 "github.com/kubeshop/testkube/internal/common" 26 "github.com/kubeshop/testkube/pkg/api/v1/client" 27 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 28 "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" 29 "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows" 30 "github.com/kubeshop/testkube/pkg/ui" 31 ) 32 33 type testExecutionDetails struct { 34 Id string `json:"id"` 35 Name string `json:"name"` 36 TestName string `json:"testName"` 37 Description string `json:"description,omitempty"` 38 } 39 40 type testWorkflowExecutionDetails struct { 41 Id string `json:"id"` 42 Name string `json:"name"` 43 TestWorkflowName string `json:"testWorkflowName"` 44 Description string `json:"description,omitempty"` 45 } 46 47 type executionResult struct { 48 Id string `json:"id"` 49 Status string `json:"status"` 50 } 51 52 func buildTestExecution(test testworkflowsv1.StepExecuteTest, async bool) (func() error, error) { 53 return func() (err error) { 54 c := env.Testkube() 55 56 if test.ExecutionRequest == nil { 57 test.ExecutionRequest = &testworkflowsv1.TestExecutionRequest{} 58 } 59 60 exec, err := c.ExecuteTest(test.Name, test.ExecutionRequest.Name, client.ExecuteTestOptions{ 61 RunningContext: &testkube.RunningContext{ 62 Type_: "testworkflow", 63 Context: fmt.Sprintf("%s/executions/%s", env.WorkflowName(), env.ExecutionId()), 64 }, 65 IsVariablesFileUploaded: test.ExecutionRequest.IsVariablesFileUploaded, 66 ExecutionLabels: test.ExecutionRequest.ExecutionLabels, 67 Command: test.ExecutionRequest.Command, 68 Args: test.ExecutionRequest.Args, 69 ArgsMode: string(test.ExecutionRequest.ArgsMode), 70 HTTPProxy: test.ExecutionRequest.HttpProxy, 71 HTTPSProxy: test.ExecutionRequest.HttpsProxy, 72 Image: test.ExecutionRequest.Image, 73 ArtifactRequest: common.MapPtr(test.ExecutionRequest.ArtifactRequest, testworkflows.MapTestArtifactRequestKubeToAPI), 74 JobTemplate: test.ExecutionRequest.JobTemplate, 75 PreRunScriptContent: test.ExecutionRequest.PreRunScript, 76 PostRunScriptContent: test.ExecutionRequest.PostRunScript, 77 ExecutePostRunScriptBeforeScraping: test.ExecutionRequest.ExecutePostRunScriptBeforeScraping, 78 SourceScripts: test.ExecutionRequest.SourceScripts, 79 ScraperTemplate: test.ExecutionRequest.ScraperTemplate, 80 NegativeTest: test.ExecutionRequest.NegativeTest, 81 EnvConfigMaps: common.MapSlice(test.ExecutionRequest.EnvConfigMaps, testworkflows.MapTestEnvReferenceKubeToAPI), 82 EnvSecrets: common.MapSlice(test.ExecutionRequest.EnvSecrets, testworkflows.MapTestEnvReferenceKubeToAPI), 83 ExecutionNamespace: test.ExecutionRequest.ExecutionNamespace, 84 }) 85 execName := exec.Name 86 if err != nil { 87 ui.Errf("failed to execute test: %s: %s", test.Name, err) 88 return 89 } 90 91 data.PrintOutput(env.Ref(), "test-start", &testExecutionDetails{ 92 Id: exec.Id, 93 Name: exec.Name, 94 TestName: exec.TestName, 95 Description: test.Description, 96 }) 97 description := "" 98 if test.Description != "" { 99 description = fmt.Sprintf(": %s", test.Description) 100 } 101 fmt.Printf("%s%s • scheduled %s\n", ui.LightCyan(execName), description, ui.DarkGray("("+exec.Id+")")) 102 103 if async { 104 return 105 } 106 107 prevStatus := testkube.QUEUED_ExecutionStatus 108 loop: 109 for { 110 time.Sleep(time.Second) 111 exec, err = c.GetExecution(exec.Id) 112 if err != nil { 113 ui.Errf("error while getting execution result: %s: %s", ui.LightCyan(execName), err.Error()) 114 return 115 } 116 if exec.ExecutionResult != nil && exec.ExecutionResult.Status != nil { 117 status := *exec.ExecutionResult.Status 118 switch status { 119 case testkube.QUEUED_ExecutionStatus, testkube.RUNNING_ExecutionStatus: 120 break 121 default: 122 break loop 123 } 124 if prevStatus != status { 125 data.PrintOutput(env.Ref(), "test-status", &executionResult{Id: exec.Id, Status: string(status)}) 126 } 127 prevStatus = status 128 } 129 } 130 131 status := *exec.ExecutionResult.Status 132 color := ui.Green 133 134 if status != testkube.PASSED_ExecutionStatus { 135 err = errors.New("test failed") 136 color = ui.Red 137 } 138 139 data.PrintOutput(env.Ref(), "test-end", &executionResult{Id: exec.Id, Status: string(status)}) 140 fmt.Printf("%s • %s\n", color(execName), string(status)) 141 return 142 }, nil 143 } 144 145 func buildWorkflowExecution(workflow testworkflowsv1.StepExecuteWorkflow, async bool) (func() error, error) { 146 return func() (err error) { 147 c := env.Testkube() 148 149 exec, err := c.ExecuteTestWorkflow(workflow.Name, testkube.TestWorkflowExecutionRequest{ 150 Name: workflow.ExecutionName, 151 Config: testworkflows.MapConfigValueKubeToAPI(workflow.Config), 152 }) 153 execName := exec.Name 154 if err != nil { 155 ui.Errf("failed to execute test workflow: %s: %s", workflow.Name, err.Error()) 156 return 157 } 158 159 data.PrintOutput(env.Ref(), "testworkflow-start", &testWorkflowExecutionDetails{ 160 Id: exec.Id, 161 Name: exec.Name, 162 TestWorkflowName: exec.Workflow.Name, 163 Description: workflow.Description, 164 }) 165 description := "" 166 if workflow.Description != "" { 167 description = fmt.Sprintf(": %s", workflow.Description) 168 } 169 fmt.Printf("%s%s • scheduled %s\n", ui.LightCyan(execName), description, ui.DarkGray("("+exec.Id+")")) 170 171 if async { 172 return 173 } 174 175 prevStatus := testkube.QUEUED_TestWorkflowStatus 176 loop: 177 for { 178 time.Sleep(100 * time.Millisecond) 179 exec, err = c.GetTestWorkflowExecution(exec.Id) 180 if err != nil { 181 ui.Errf("error while getting execution result: %s: %s", ui.LightCyan(execName), err.Error()) 182 return 183 } 184 if exec.Result != nil && exec.Result.Status != nil { 185 status := *exec.Result.Status 186 switch status { 187 case testkube.QUEUED_TestWorkflowStatus, testkube.RUNNING_TestWorkflowStatus: 188 break 189 default: 190 break loop 191 } 192 if prevStatus != status { 193 data.PrintOutput(env.Ref(), "testworkflow-status", &executionResult{Id: exec.Id, Status: string(status)}) 194 } 195 prevStatus = status 196 } 197 } 198 199 status := *exec.Result.Status 200 color := ui.Green 201 202 if status != testkube.PASSED_TestWorkflowStatus { 203 err = errors.New("test workflow failed") 204 color = ui.Red 205 } 206 207 data.PrintOutput(env.Ref(), "testworkflow-end", &executionResult{Id: exec.Id, Status: string(status)}) 208 fmt.Printf("%s • %s\n", color(execName), string(status)) 209 return 210 }, nil 211 } 212 213 func NewExecuteCmd() *cobra.Command { 214 var ( 215 tests []string 216 workflows []string 217 parallelism int 218 async bool 219 ) 220 221 cmd := &cobra.Command{ 222 Use: "execute", 223 Short: "Execute other resources", 224 Args: cobra.ExactArgs(0), 225 226 Run: func(cmd *cobra.Command, _ []string) { 227 // Initialize internal machine 228 baseMachine := data.GetBaseTestWorkflowMachine() 229 230 // Build operations to run 231 operations := make([]func() error, 0) 232 for _, s := range tests { 233 var t testworkflowsv1.StepExecuteTest 234 err := json.Unmarshal([]byte(s), &t) 235 if err != nil { 236 ui.Fail(errors.Wrap(err, "unmarshal test definition")) 237 } 238 239 // Resolve the params 240 params, err := common2.GetParamsSpec(t.Matrix, t.Shards, t.Count, t.MaxCount, baseMachine) 241 if err != nil { 242 ui.Fail(errors.Wrap(err, "matrix and sharding")) 243 } 244 fmt.Printf("%s: %s\n", common2.ServiceLabel(t.Name), params.Humanize()) 245 246 // Create operations for each expected execution 247 for i := int64(0); i < params.Count; i++ { 248 spec := t.DeepCopy() 249 err := expressionstcl.Finalize(&spec, baseMachine, params.MachineAt(i)) 250 if err != nil { 251 ui.Fail(errors.Wrapf(err, "'%s' test: computing execution", spec.Name)) 252 } 253 fn, err := buildTestExecution(*spec, async) 254 if err != nil { 255 ui.Fail(err) 256 } 257 operations = append(operations, fn) 258 } 259 } 260 for _, s := range workflows { 261 var w testworkflowsv1.StepExecuteWorkflow 262 err := json.Unmarshal([]byte(s), &w) 263 if err != nil { 264 ui.Fail(errors.Wrap(err, "unmarshal workflow definition")) 265 } 266 267 // Resolve the params 268 params, err := common2.GetParamsSpec(w.Matrix, w.Shards, w.Count, w.MaxCount, baseMachine) 269 if err != nil { 270 ui.Fail(errors.Wrap(err, "matrix and sharding")) 271 } 272 fmt.Printf("%s: %s\n", common2.ServiceLabel(w.Name), params.Humanize()) 273 274 // Create operations for each expected execution 275 for i := int64(0); i < params.Count; i++ { 276 spec := w.DeepCopy() 277 err := expressionstcl.Finalize(&spec, baseMachine, params.MachineAt(i)) 278 if err != nil { 279 ui.Fail(errors.Wrapf(err, "'%s' workflow: computing execution", spec.Name)) 280 } 281 fn, err := buildWorkflowExecution(*spec, async) 282 if err != nil { 283 ui.Fail(err) 284 } 285 operations = append(operations, fn) 286 } 287 } 288 289 // Validate if there is anything to run 290 if len(operations) == 0 { 291 fmt.Printf("nothing to run\n") 292 os.Exit(0) 293 } 294 295 // Calculate parallelism 296 if parallelism <= 0 { 297 parallelism = 100 298 } 299 if parallelism < len(operations) { 300 fmt.Printf("Total: %d executions, %d parallel\n", len(operations), parallelism) 301 } else { 302 fmt.Printf("Total: %d executions, all in parallel\n", len(operations)) 303 } 304 305 // Create channel for execution 306 var wg sync.WaitGroup 307 wg.Add(len(operations)) 308 ch := make(chan struct{}, parallelism) 309 success := true 310 311 // Execute all operations 312 for _, op := range operations { 313 ch <- struct{}{} 314 go func(op func() error) { 315 if op() != nil { 316 success = false 317 } 318 <-ch 319 wg.Done() 320 }(op) 321 } 322 wg.Wait() 323 324 if !success { 325 os.Exit(1) 326 } 327 }, 328 } 329 330 // TODO: Support test suites too 331 cmd.Flags().StringArrayVarP(&tests, "test", "t", nil, "tests to run") 332 cmd.Flags().StringArrayVarP(&workflows, "workflow", "w", nil, "workflows to run") 333 cmd.Flags().IntVarP(¶llelism, "parallelism", "p", 0, "how many items could be executed at once") 334 cmd.Flags().BoolVar(&async, "async", false, "should it wait for results") 335 336 return cmd 337 }