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(&parallelism, "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  }