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  }