github.com/kubeshop/testkube@v1.17.23/cmd/tcl/testworkflow-init/main.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 main
    10  
    11  import (
    12  	"fmt"
    13  	"os"
    14  	"os/signal"
    15  	"slices"
    16  	"strings"
    17  	"syscall"
    18  	"time"
    19  
    20  	"github.com/kballard/go-shellquote"
    21  
    22  	"github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/constants"
    23  	"github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/data"
    24  	"github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/output"
    25  	"github.com/kubeshop/testkube/cmd/tcl/testworkflow-init/run"
    26  )
    27  
    28  func main() {
    29  	if len(os.Args) < 2 {
    30  		output.Failf(output.CodeInputError, "missing step reference")
    31  	}
    32  	data.Step.Ref = os.Args[1]
    33  
    34  	now := time.Now()
    35  
    36  	// Load shared state
    37  	data.LoadState()
    38  
    39  	// Initialize space for parsing args
    40  	config := map[string]string{}
    41  	computed := []string(nil)
    42  	conditions := []data.Rule(nil)
    43  	resulting := []data.Rule(nil)
    44  	timeouts := []data.Timeout(nil)
    45  	args := []string(nil)
    46  
    47  	// Read arguments into the base data
    48  	for i := 2; i < len(os.Args); i += 2 {
    49  		if i+1 == len(os.Args) {
    50  			break
    51  		}
    52  		switch os.Args[i] {
    53  		case constants.ArgSeparator:
    54  			args = os.Args[i+1:]
    55  			i = len(os.Args)
    56  		case constants.ArgInit, constants.ArgInitLong:
    57  			data.Step.InitStatus = os.Args[i+1]
    58  		case constants.ArgCondition, constants.ArgConditionLong:
    59  			v := strings.SplitN(os.Args[i+1], "=", 2)
    60  			refs := strings.Split(v[0], ",")
    61  			if len(v) == 2 {
    62  				conditions = append(conditions, data.Rule{Expr: v[1], Refs: refs})
    63  			} else {
    64  				conditions = append(conditions, data.Rule{Expr: "true", Refs: refs})
    65  			}
    66  		case constants.ArgResult, constants.ArgResultLong:
    67  			v := strings.SplitN(os.Args[i+1], "=", 2)
    68  			refs := strings.Split(v[0], ",")
    69  			if len(v) == 2 {
    70  				resulting = append(resulting, data.Rule{Expr: v[1], Refs: refs})
    71  			} else {
    72  				resulting = append(resulting, data.Rule{Expr: "true", Refs: refs})
    73  			}
    74  		case constants.ArgTimeout, constants.ArgTimeoutLong:
    75  			v := strings.SplitN(os.Args[i+1], "=", 2)
    76  			if len(v) == 2 {
    77  				timeouts = append(timeouts, data.Timeout{Ref: v[0], Duration: v[1]})
    78  			} else {
    79  				timeouts = append(timeouts, data.Timeout{Ref: v[0], Duration: ""})
    80  			}
    81  		case constants.ArgComputeEnv, constants.ArgComputeEnvLong:
    82  			computed = append(computed, strings.Split(os.Args[i+1], ",")...)
    83  		case constants.ArgNegative, constants.ArgNegativeLong:
    84  			config["negative"] = os.Args[i+1]
    85  		case constants.ArgRetryCount:
    86  			config["retryCount"] = os.Args[i+1]
    87  		case constants.ArgRetryUntil:
    88  			config["retryUntil"] = os.Args[i+1]
    89  		case constants.ArgDebug:
    90  			config["debug"] = os.Args[i+1]
    91  		default:
    92  			output.Failf(output.CodeInputError, "unknown parameter: %s", os.Args[i])
    93  		}
    94  	}
    95  
    96  	// Compute environment variables
    97  	for _, name := range computed {
    98  		initial := os.Getenv(name)
    99  		value, err := data.Template(initial)
   100  		if err != nil {
   101  			output.Failf(output.CodeInputError, `resolving "%s" environment variable: %s: %s`, name, initial, err.Error())
   102  		}
   103  		_ = os.Setenv(name, value)
   104  	}
   105  
   106  	// Compute conditional steps - ignore errors initially, as the may be dependent on themselves
   107  	data.Iterate(conditions, func(c data.Rule) bool {
   108  		expr, err := data.Expression(c.Expr)
   109  		if err != nil {
   110  			return false
   111  		}
   112  		v, _ := expr.BoolValue()
   113  		if !v {
   114  			for _, r := range c.Refs {
   115  				data.State.GetStep(r).Skip(now)
   116  			}
   117  		}
   118  		return true
   119  	})
   120  
   121  	// Fail invalid conditional steps
   122  	for _, c := range conditions {
   123  		_, err := data.Expression(c.Expr)
   124  		if err != nil {
   125  			output.Failf(output.CodeInputError, "broken condition for refs: %s: %s: %s", strings.Join(c.Refs, ", "), c.Expr, err.Error())
   126  		}
   127  	}
   128  
   129  	// Start all acknowledged steps
   130  	for _, f := range resulting {
   131  		for _, r := range f.Refs {
   132  			if r != "" {
   133  				data.State.GetStep(r).Start(now)
   134  			}
   135  		}
   136  	}
   137  	for _, t := range timeouts {
   138  		if t.Ref != "" {
   139  			data.State.GetStep(t.Ref).Start(now)
   140  		}
   141  	}
   142  	data.State.GetStep(data.Step.Ref).Start(now)
   143  
   144  	// Register timeouts
   145  	for _, t := range timeouts {
   146  		err := data.State.GetStep(t.Ref).SetTimeoutDuration(now, t.Duration)
   147  		if err != nil {
   148  			output.Failf(output.CodeInputError, "broken timeout for ref: %s: %s: %s", t.Ref, t.Duration, err.Error())
   149  		}
   150  	}
   151  
   152  	// Save the resulting conditions
   153  	data.Config.Resulting = resulting
   154  
   155  	// Don't call further if the step is already skipped
   156  	if data.State.GetStep(data.Step.Ref).Status == data.StepStatusSkipped {
   157  		if data.Config.Debug {
   158  			fmt.Printf("Skipped.\n")
   159  		}
   160  		data.Finish()
   161  	}
   162  
   163  	// Load the rest of the configuration
   164  	var err error
   165  	for k, v := range config {
   166  		config[k], err = data.Template(v)
   167  		if err != nil {
   168  			output.Failf(output.CodeInputError, `resolving "%s" param: %s: %s`, k, v, err.Error())
   169  		}
   170  	}
   171  	data.LoadConfig(config)
   172  
   173  	// Compute templates in the cmd/args
   174  	original := slices.Clone(args)
   175  	for i := range args {
   176  		args[i], err = data.Template(args[i])
   177  		if err != nil {
   178  			output.Failf(output.CodeInputError, `resolving command: %s: %s`, shellquote.Join(original...), err.Error())
   179  		}
   180  	}
   181  
   182  	// Fail when there is nothing to run
   183  	if len(args) == 0 {
   184  		output.Failf(output.CodeNoCommand, "missing command to run")
   185  	}
   186  
   187  	// Handle aborting
   188  	stopSignal := make(chan os.Signal, 1)
   189  	signal.Notify(stopSignal, syscall.SIGINT, syscall.SIGTERM)
   190  	go func() {
   191  		<-stopSignal
   192  		fmt.Println("The task was aborted.")
   193  		data.Step.Status = data.StepStatusAborted
   194  		data.Step.ExitCode = output.CodeAborted
   195  		data.Finish()
   196  	}()
   197  
   198  	// Handle timeouts
   199  	for _, t := range timeouts {
   200  		go func(ref string) {
   201  			time.Sleep(data.State.GetStep(ref).TimeoutAt.Sub(time.Now()))
   202  			fmt.Printf("Timed out.\n")
   203  			data.State.GetStep(ref).SetStatus(data.StepStatusTimeout)
   204  			data.Step.Status = data.StepStatusTimeout
   205  			data.Step.ExitCode = output.CodeTimeout
   206  			data.Finish()
   207  		}(t.Ref)
   208  	}
   209  
   210  	// Start the task
   211  	data.Step.Executed = true
   212  	run.Run(args[0], args[1:])
   213  
   214  	os.Exit(0)
   215  }