github.com/kubeshop/testkube@v1.17.23/cmd/kubectl-testkube/commands/testworkflows/run.go (about) 1 package testworkflows 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 "time" 8 9 "github.com/spf13/cobra" 10 11 "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common" 12 "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render" 13 "github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/testworkflows/renderer" 14 apiclientv1 "github.com/kubeshop/testkube/pkg/api/v1/client" 15 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 16 "github.com/kubeshop/testkube/pkg/ui" 17 ) 18 19 const ( 20 LogTimestampLength = 30 // time.RFC3339Nano without 00:00 timezone 21 ) 22 23 func NewRunTestWorkflowCmd() *cobra.Command { 24 var ( 25 executionName string 26 config map[string]string 27 watchEnabled bool 28 ) 29 30 cmd := &cobra.Command{ 31 Use: "testworkflow [name]", 32 Aliases: []string{"testworkflows", "tw"}, 33 Args: cobra.ExactArgs(1), 34 Short: "Starts test workflow execution", 35 36 Run: func(cmd *cobra.Command, args []string) { 37 namespace := cmd.Flag("namespace").Value.String() 38 client, _, err := common.GetClient(cmd) 39 ui.ExitOnError("getting client", err) 40 41 name := args[0] 42 execution, err := client.ExecuteTestWorkflow(name, testkube.TestWorkflowExecutionRequest{ 43 Name: executionName, 44 Config: config, 45 }) 46 ui.ExitOnError("execute test workflow "+name+" from namespace "+namespace, err) 47 err = render.Obj(cmd, execution, os.Stdout, renderer.TestWorkflowExecutionRenderer) 48 ui.ExitOnError("render test workflow execution", err) 49 50 ui.NL() 51 var exitCode = 0 52 if watchEnabled { 53 exitCode = uiWatch(execution, client) 54 ui.NL() 55 } else { 56 uiShellWatchExecution(execution.Id) 57 } 58 59 uiShellGetExecution(execution.Id) 60 os.Exit(exitCode) 61 }, 62 } 63 64 cmd.Flags().StringVarP(&executionName, "name", "n", "", "execution name, if empty will be autogenerated") 65 cmd.Flags().StringToStringVarP(&config, "config", "", map[string]string{}, "configuration variables in a form of name1=val1 passed to executor") 66 cmd.Flags().BoolVarP(&watchEnabled, "watch", "f", false, "watch for changes after start") 67 68 return cmd 69 } 70 71 func uiWatch(execution testkube.TestWorkflowExecution, client apiclientv1.Client) int { 72 result, err := watchTestWorkflowLogs(execution.Id, execution.Signature, client) 73 ui.ExitOnError("reading test workflow execution logs", err) 74 75 // Apply the result in the execution 76 execution.Result = result 77 if result.IsFinished() { 78 execution.StatusAt = result.FinishedAt 79 } 80 81 // Display message depending on the result 82 switch { 83 case result.Initialization.ErrorMessage != "": 84 ui.Warn("test workflow execution failed:\n") 85 ui.Errf(result.Initialization.ErrorMessage) 86 return 1 87 case result.IsFailed(): 88 ui.Warn("test workflow execution failed") 89 return 1 90 case result.IsAborted(): 91 ui.Warn("test workflow execution aborted") 92 return 1 93 case result.IsPassed(): 94 ui.Success("test workflow execution completed with success in " + result.FinishedAt.Sub(result.QueuedAt).String()) 95 } 96 return 0 97 } 98 99 func uiShellGetExecution(id string) { 100 ui.ShellCommand( 101 "Use following command to get test workflow execution details", 102 "kubectl testkube get twe "+id, 103 ) 104 } 105 106 func uiShellWatchExecution(id string) { 107 ui.ShellCommand( 108 "Watch test workflow execution until complete", 109 "kubectl testkube watch twe "+id, 110 ) 111 } 112 113 func flattenSignatures(sig []testkube.TestWorkflowSignature) []testkube.TestWorkflowSignature { 114 res := make([]testkube.TestWorkflowSignature, 0) 115 for _, s := range sig { 116 if len(s.Children) == 0 { 117 res = append(res, s) 118 } else { 119 res = append(res, flattenSignatures(s.Children)...) 120 } 121 } 122 return res 123 } 124 125 func printResultDifference(res1 *testkube.TestWorkflowResult, res2 *testkube.TestWorkflowResult, steps []testkube.TestWorkflowSignature) bool { 126 if res1 == nil || res2 == nil { 127 return false 128 } 129 changed := false 130 for i, s := range steps { 131 r1 := res1.Steps[s.Ref] 132 r2 := res2.Steps[s.Ref] 133 r1Status := testkube.QUEUED_TestWorkflowStepStatus 134 r2Status := testkube.QUEUED_TestWorkflowStepStatus 135 if r1.Status != nil { 136 r1Status = *r1.Status 137 } 138 if r2.Status != nil { 139 r2Status = *r2.Status 140 } 141 if r1Status == r2Status { 142 continue 143 } 144 name := s.Category 145 if s.Name != "" { 146 name = s.Name 147 } 148 took := r2.FinishedAt.Sub(r2.QueuedAt).Round(time.Millisecond) 149 changed = true 150 151 switch r2Status { 152 case testkube.RUNNING_TestWorkflowStepStatus: 153 fmt.Print(ui.LightCyan(fmt.Sprintf("\n• (%d/%d) %s\n", i+1, len(steps), name))) 154 case testkube.SKIPPED_TestWorkflowStepStatus: 155 fmt.Print(ui.LightGray("• skipped\n")) 156 case testkube.PASSED_TestWorkflowStepStatus: 157 fmt.Print(ui.Green(fmt.Sprintf("\n• passed in %s\n", took))) 158 case testkube.ABORTED_TestWorkflowStepStatus: 159 fmt.Print(ui.Red("\n• aborted\n")) 160 default: 161 if s.Optional { 162 fmt.Print(ui.Yellow(fmt.Sprintf("\n• %s in %s (ignored)\n", string(r2Status), took))) 163 } else { 164 fmt.Print(ui.Red(fmt.Sprintf("\n• %s in %s\n", string(r2Status), took))) 165 } 166 } 167 } 168 169 return changed 170 } 171 172 func watchTestWorkflowLogs(id string, signature []testkube.TestWorkflowSignature, client apiclientv1.Client) (*testkube.TestWorkflowResult, error) { 173 ui.Info("Getting logs from test workflow job", id) 174 175 notifications, err := client.GetTestWorkflowExecutionNotifications(id) 176 ui.ExitOnError("getting logs from executor", err) 177 178 steps := flattenSignatures(signature) 179 180 var result *testkube.TestWorkflowResult 181 var isLineBeginning = true 182 for l := range notifications { 183 if l.Output != nil { 184 continue 185 } 186 if l.Result != nil { 187 isLineBeginning = printResultDifference(result, l.Result, steps) 188 result = l.Result 189 continue 190 } 191 192 // Strip timestamp + space for all new lines in the log 193 for len(l.Log) > 0 { 194 if isLineBeginning { 195 l.Log = l.Log[LogTimestampLength+1:] 196 isLineBeginning = false 197 } 198 newLineIndex := strings.Index(l.Log, "\n") 199 if newLineIndex == -1 { 200 fmt.Print(l.Log) 201 break 202 } else { 203 fmt.Print(l.Log[0 : newLineIndex+1]) 204 l.Log = l.Log[newLineIndex+1:] 205 isLineBeginning = true 206 } 207 } 208 } 209 210 ui.NL() 211 212 return result, err 213 }