github.com/kubeshop/testkube@v1.17.23/contrib/executor/curl/pkg/runner/runner.go (about)

     1  package runner
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/pkg/errors"
    14  	"go.uber.org/zap"
    15  
    16  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    17  	"github.com/kubeshop/testkube/pkg/envs"
    18  	"github.com/kubeshop/testkube/pkg/executor"
    19  	"github.com/kubeshop/testkube/pkg/executor/agent"
    20  	contentPkg "github.com/kubeshop/testkube/pkg/executor/content"
    21  	"github.com/kubeshop/testkube/pkg/executor/env"
    22  	"github.com/kubeshop/testkube/pkg/executor/output"
    23  	outputPkg "github.com/kubeshop/testkube/pkg/executor/output"
    24  	"github.com/kubeshop/testkube/pkg/executor/runner"
    25  	"github.com/kubeshop/testkube/pkg/executor/scraper"
    26  	"github.com/kubeshop/testkube/pkg/executor/scraper/factory"
    27  	"github.com/kubeshop/testkube/pkg/log"
    28  	"github.com/kubeshop/testkube/pkg/ui"
    29  )
    30  
    31  // CurlRunner is used to run curl commands.
    32  type CurlRunner struct {
    33  	Params  envs.Params
    34  	Log     *zap.SugaredLogger
    35  	Scraper scraper.Scraper
    36  }
    37  
    38  var _ runner.Runner = &CurlRunner{}
    39  
    40  func NewCurlRunner(ctx context.Context, params envs.Params) (*CurlRunner, error) {
    41  	outputPkg.PrintLogf("%s Preparing test runner", ui.IconTruck)
    42  
    43  	var err error
    44  	r := &CurlRunner{
    45  		Log:    log.DefaultLogger,
    46  		Params: params,
    47  	}
    48  
    49  	r.Scraper, err = factory.TryGetScrapper(ctx, params)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	return r, nil
    55  }
    56  
    57  func (r *CurlRunner) Run(ctx context.Context, execution testkube.Execution) (result testkube.ExecutionResult, err error) {
    58  	if r.Scraper != nil {
    59  		defer r.Scraper.Close()
    60  	}
    61  
    62  	outputPkg.PrintLogf("%s Preparing for test run", ui.IconTruck)
    63  	var runnerInput CurlRunnerInput
    64  
    65  	path, workingDir, err := contentPkg.GetPathAndWorkingDir(execution.Content, r.Params.DataDir)
    66  	if err != nil {
    67  		outputPkg.PrintLogf("%s Failed to resolve absolute directory for %s, using the path directly", ui.IconWarning, r.Params.DataDir)
    68  	}
    69  
    70  	fileInfo, err := os.Stat(path)
    71  	if err != nil {
    72  		return result, err
    73  	}
    74  
    75  	if fileInfo.IsDir() {
    76  		scriptName := execution.Args[len(execution.Args)-1]
    77  		if workingDir != "" {
    78  			path = ""
    79  			if execution.Content != nil && execution.Content.Repository != nil {
    80  				scriptName = filepath.Join(execution.Content.Repository.Path, scriptName)
    81  			}
    82  		}
    83  
    84  		execution.Args = execution.Args[:len(execution.Args)-1]
    85  		output.PrintLogf("%s It is a directory test - trying to find file from the last executor argument %s in directory %s", ui.IconWorld, scriptName, path)
    86  
    87  		// sanity checking for test script
    88  		scriptFile := filepath.Join(path, workingDir, scriptName)
    89  		fileInfo, errFile := os.Stat(scriptFile)
    90  		if errors.Is(errFile, os.ErrNotExist) || fileInfo.IsDir() {
    91  			output.PrintLogf("%s Could not find file %s in the directory, error: %s", ui.IconCross, scriptName, errFile)
    92  			return *result.Err(errors.Errorf("could not find file %s in the directory: %v", scriptName, errFile)), nil
    93  		}
    94  		path = scriptFile
    95  	}
    96  
    97  	content, err := os.ReadFile(path)
    98  	if err != nil {
    99  		return result, err
   100  	}
   101  
   102  	err = json.Unmarshal(content, &runnerInput)
   103  	if err != nil {
   104  		return result, err
   105  	}
   106  
   107  	envManager := env.NewManagerWithVars(execution.Variables)
   108  	envManager.GetReferenceVars(envManager.Variables)
   109  	variables := testkube.VariablesToMap(envManager.Variables)
   110  
   111  	outputPkg.PrintLogf("%s Filling in the input templates", ui.IconKey)
   112  	err = runnerInput.FillTemplates(variables)
   113  	if err != nil {
   114  		outputPkg.PrintLogf("%s Failed to fill in the input templates: %s", ui.IconCross, err.Error())
   115  		r.Log.Errorf("Error occured when resolving input templates %s", err)
   116  		return *result.Err(err), nil
   117  	}
   118  	outputPkg.PrintLogf("%s Successfully filled the input templates", ui.IconCheckMark)
   119  
   120  	command := ""
   121  	var args []string
   122  	if len(execution.Command) != 0 {
   123  		command = execution.Command[0]
   124  		args = execution.Command[1:]
   125  	}
   126  
   127  	if len(runnerInput.Command) != 0 {
   128  		command = runnerInput.Command[0]
   129  		args = runnerInput.Command[1:]
   130  	}
   131  
   132  	if command != "curl" {
   133  		outputPkg.PrintLogf("%s you can run only `curl` commands with this executor but passed: `%s`", ui.IconCross, command)
   134  		return result, errors.Errorf("you can run only `curl` commands with this executor but passed: `%s`", command)
   135  	}
   136  
   137  	args = append(args, execution.Args...)
   138  	for i := range args {
   139  		args[i] = os.ExpandEnv(args[i])
   140  	}
   141  
   142  	runPath := workingDir
   143  	outputPkg.PrintLogf("%s Test run command %s %s", ui.IconRocket, command, strings.Join(envManager.ObfuscateStringSlice(args), " "))
   144  	output, err := executor.Run(runPath, command, envManager, args...)
   145  	output = envManager.ObfuscateSecrets(output)
   146  
   147  	if err != nil {
   148  		r.Log.Errorf("Error occured when running a command %s", err)
   149  		return *result.Err(err), nil
   150  	}
   151  
   152  	var rerr error
   153  	if execution.PostRunScript != "" && execution.ExecutePostRunScriptBeforeScraping {
   154  		outputPkg.PrintLog(fmt.Sprintf("%s Running post run script...", ui.IconCheckMark))
   155  
   156  		if rerr = agent.RunScript(execution.PostRunScript, r.Params.WorkingDir); rerr != nil {
   157  			outputPkg.PrintLogf("%s Failed to execute post run script %s", ui.IconWarning, rerr)
   158  		}
   159  	}
   160  
   161  	// scrape artifacts first even if there are errors above
   162  	if r.Params.ScrapperEnabled && execution.ArtifactRequest != nil && len(execution.ArtifactRequest.Dirs) != 0 {
   163  		outputPkg.PrintLogf("Scraping directories: %v with masks: %v", execution.ArtifactRequest.Dirs, execution.ArtifactRequest.Masks)
   164  
   165  		if err := r.Scraper.Scrape(ctx, execution.ArtifactRequest.Dirs, execution.ArtifactRequest.Masks, execution); err != nil {
   166  			return *result.WithErrors(err), nil
   167  		}
   168  	}
   169  
   170  	outputString := string(output)
   171  	result.Output = outputString
   172  	responseStatus, err := getResponseCode(outputString)
   173  	if err != nil {
   174  		outputPkg.PrintLogf("%s Test run failed: %s", ui.IconCross, err.Error())
   175  		return *result.Err(err), nil
   176  	}
   177  
   178  	expectedStatus, err := strconv.Atoi(runnerInput.ExpectedStatus)
   179  	if err != nil {
   180  		outputPkg.PrintLogf("%s Test run failed: cannot process expected status: %s", ui.IconCross, err.Error())
   181  		return *result.Err(errors.Errorf("cannot process expected status %s", runnerInput.ExpectedStatus)), nil
   182  	}
   183  
   184  	if responseStatus != expectedStatus {
   185  		outputPkg.PrintLogf("%s Test run failed: response status don't match: expected %d got %d", ui.IconCross, expectedStatus, responseStatus)
   186  		return *result.Err(errors.Errorf("response status don't match expected %d got %d", expectedStatus, responseStatus)), nil
   187  	}
   188  
   189  	if !strings.Contains(outputString, runnerInput.ExpectedBody) {
   190  		outputPkg.PrintLogf("%s Test run failed: response doesn't contain body: %s", ui.IconCross, runnerInput.ExpectedBody)
   191  		return *result.Err(errors.Errorf("response doesn't contain body: %s", runnerInput.ExpectedBody)), nil
   192  	}
   193  
   194  	if rerr != nil {
   195  		return *result.Err(rerr), nil
   196  	}
   197  
   198  	outputPkg.PrintLogf("%s Test run succeeded", ui.IconCheckMark)
   199  
   200  	return testkube.ExecutionResult{
   201  		Status: testkube.ExecutionStatusPassed,
   202  		Output: outputString,
   203  	}, nil
   204  }
   205  
   206  func getResponseCode(curlOutput string) (int, error) {
   207  	re := regexp.MustCompile(`\A\S*\s(\d+)`)
   208  	matches := re.FindStringSubmatch(curlOutput)
   209  	if len(matches) == 0 {
   210  		return -1, errors.Errorf("could not find a response status in the command output")
   211  	}
   212  	return strconv.Atoi(matches[1])
   213  }
   214  
   215  // GetType returns runner type
   216  func (r *CurlRunner) GetType() runner.Type {
   217  	return runner.TypeMain
   218  }