github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/test/custom/custom.go (about) 1 /* 2 Copyright 2021 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package custom 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "os/exec" 25 "path/filepath" 26 "runtime" 27 "time" 28 29 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/list" 30 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker" 31 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/event" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 35 ) 36 37 var ( 38 // for tests 39 testContext = retrieveTestContext 40 ) 41 42 const Windows string = "windows" 43 44 type Runner struct { 45 cfg docker.Config 46 customTest latest.CustomTest 47 imageName string 48 workspace string 49 } 50 51 // New creates a new custom.Runner. 52 func New(cfg docker.Config, imageName string, ws string, ct latest.CustomTest) (*Runner, error) { 53 return &Runner{ 54 cfg: cfg, 55 imageName: imageName, 56 customTest: ct, 57 workspace: ws, 58 }, nil 59 } 60 61 // Test is the entrypoint for running custom tests 62 func (ct *Runner) Test(ctx context.Context, out io.Writer, imageTag string) error { 63 event.TestInProgress() 64 if err := ct.runCustomTest(ctx, out, imageTag); err != nil { 65 event.TestFailed(ct.imageName, err) 66 return err 67 } 68 event.TestComplete() 69 return nil 70 } 71 72 func (ct *Runner) runCustomTest(ctx context.Context, out io.Writer, imageTag string) error { 73 test := ct.customTest 74 75 // Expand command 76 command, err := util.ExpandEnvTemplate(test.Command, nil) 77 if err != nil { 78 return cmdRunParsingErr(test.Command, err) 79 } 80 81 if test.TimeoutSeconds <= 0 { 82 output.Default.Fprintf(out, "Running custom test command: %q\n", command) 83 } else { 84 output.Default.Fprintf(out, "Running custom test command: %q with timeout %d s\n", command, test.TimeoutSeconds) 85 newCtx, cancel := context.WithTimeout(ctx, time.Duration(test.TimeoutSeconds)*time.Second) 86 87 defer cancel() 88 ctx = newCtx 89 } 90 91 cmd, err := ct.retrieveCmd(ctx, out, command, imageTag) 92 if err != nil { 93 return cmdRunRetrieveErr(command, ct.imageName, err) 94 } 95 96 if err := util.RunCmd(ctx, cmd); err != nil { 97 if e, ok := err.(*exec.ExitError); ok { 98 // If the process exited by itself, just return the error 99 if e.Exited() { 100 output.Red.Fprintf(out, "Command finished with non-0 exit code.\n") 101 return cmdRunNonZeroExitErr(command, e) 102 } 103 // If the context is done, it has been killed by the exec.Command 104 select { 105 case <-ctx.Done(): 106 if ctx.Err() == context.DeadlineExceeded { 107 output.Red.Fprintf(out, "Command timed out\n") 108 return cmdRunTimedoutErr(test.TimeoutSeconds, ctx.Err()) 109 } else if ctx.Err() == context.Canceled { 110 output.Red.Fprintf(out, "Command cancelled\n") 111 return cmdRunCancelledErr(ctx.Err()) 112 } 113 return cmdRunExecutionErr(ctx.Err()) 114 default: 115 return cmdRunExited(e) 116 } 117 } 118 return cmdRunErr(err) 119 } 120 output.Green.Fprintf(out, "Command finished successfully.\n") 121 122 return nil 123 } 124 125 // TestDependencies returns dependencies listed for a custom test 126 func (ct *Runner) TestDependencies(ctx context.Context) ([]string, error) { 127 test := ct.customTest 128 129 if test.Dependencies != nil { 130 switch { 131 case test.Dependencies.Command != "": 132 var cmd *exec.Cmd 133 // We evaluate the command with a shell so that it can contain env variables. 134 if runtime.GOOS == Windows { 135 cmd = exec.CommandContext(context.Background(), "cmd.exe", "/C", test.Dependencies.Command) 136 } else { 137 cmd = exec.CommandContext(context.Background(), "sh", "-c", test.Dependencies.Command) 138 } 139 140 output, err := util.RunCmdOut(ctx, cmd) 141 if err != nil { 142 return nil, gettingDependenciesCommandErr(test.Dependencies.Command, err) 143 } 144 var deps []string 145 if err := json.Unmarshal(output, &deps); err != nil { 146 return nil, dependencyOutputUnmarshallErr(test.Dependencies.Paths, err) 147 } 148 return deps, nil 149 150 case test.Dependencies.Paths != nil: 151 return list.Files(ct.workspace, test.Dependencies.Paths, test.Dependencies.Ignore) 152 } 153 } 154 return nil, nil 155 } 156 157 func (ct *Runner) retrieveCmd(ctx context.Context, out io.Writer, command string, imageTag string) (*exec.Cmd, error) { 158 var cmd *exec.Cmd 159 // We evaluate the command with a shell so that it can contain env variables. 160 161 if runtime.GOOS == Windows { 162 cmd = exec.CommandContext(ctx, "cmd.exe", "/C", command) 163 } else { 164 cmd = exec.CommandContext(ctx, "sh", "-c", command) 165 } 166 cmd.Stdout = out 167 cmd.Stderr = out 168 169 env, err := ct.getEnv(ctx, imageTag) 170 if err != nil { 171 return nil, fmt.Errorf("setting env variables: %w", err) 172 } 173 cmd.Env = env 174 175 dir, err := testContext(ct.workspace) 176 if err != nil { 177 return nil, fmt.Errorf("getting context for test: %w", err) 178 } 179 cmd.Dir = dir 180 181 return cmd, nil 182 } 183 184 func (ct *Runner) getEnv(ctx context.Context, imageTag string) ([]string, error) { 185 testContext, err := testContext(ct.workspace) 186 if err != nil { 187 return nil, fmt.Errorf("getting absolute path for test context: %w", err) 188 } 189 190 envs := []string{ 191 fmt.Sprintf("%s=%s", "IMAGE", imageTag), 192 fmt.Sprintf("%s=%s", "TEST_CONTEXT", testContext), 193 } 194 195 envs = append(envs, util.OSEnviron()...) 196 197 // add minikube docker env vars to command env context if minikube cluster detected 198 localDaemon, err := docker.NewAPIClient(ctx, ct.cfg) 199 if err != nil { 200 return nil, fmt.Errorf("getting docker client information: %w", err) 201 } 202 envs = append(envs, localDaemon.ExtraEnv()...) 203 204 return envs, nil 205 } 206 207 func retrieveTestContext(workspace string) (string, error) { 208 return filepath.Abs(workspace) 209 }