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 }