github.com/metasources/buildx@v0.0.0-20230418141019-7aa1459cedea/test/cli/utils_test.go (about) 1 package cli 2 3 import ( 4 "bufio" 5 "bytes" 6 "flag" 7 "fmt" 8 "io" 9 "math" 10 "os" 11 "os/exec" 12 "path" 13 "path/filepath" 14 "runtime" 15 "strings" 16 "syscall" 17 "testing" 18 "time" 19 20 "github.com/stretchr/testify/require" 21 22 "github.com/anchore/stereoscope/pkg/imagetest" 23 ) 24 25 var showOutput = flag.Bool("show-output", false, "show stdout and stderr for failing tests") 26 27 func logOutputOnFailure(t testing.TB, cmd *exec.Cmd, stdout, stderr string) { 28 if t.Failed() && showOutput != nil && *showOutput { 29 t.Log("STDOUT:\n", stdout) 30 t.Log("STDERR:\n", stderr) 31 t.Log("COMMAND:", strings.Join(cmd.Args, " ")) 32 } 33 } 34 35 func runAndShow(t *testing.T, cmd *exec.Cmd) { 36 t.Helper() 37 38 stderr, err := cmd.StderrPipe() 39 require.NoErrorf(t, err, "could not get stderr: +v", err) 40 41 stdout, err := cmd.StdoutPipe() 42 require.NoErrorf(t, err, "could not get stdout: +v", err) 43 44 err = cmd.Start() 45 require.NoErrorf(t, err, "failed to start cmd: %+v", err) 46 47 show := func(label string, reader io.ReadCloser) { 48 scanner := bufio.NewScanner(reader) 49 scanner.Split(bufio.ScanLines) 50 for scanner.Scan() { 51 t.Logf("%s: %s", label, scanner.Text()) 52 } 53 } 54 55 show("out", stdout) 56 show("err", stderr) 57 } 58 59 func setupPKI(t *testing.T, pw string) func() { 60 err := os.Setenv("COSIGN_PASSWORD", pw) 61 if err != nil { 62 t.Fatal(err) 63 } 64 65 cosignPath := filepath.Join(repoRoot(t), ".tmp/cosign") 66 cmd := exec.Command(cosignPath, "generate-key-pair") 67 stdout, stderr, _ := runCommand(cmd, nil) 68 if cmd.ProcessState.ExitCode() != 0 { 69 t.Log("STDOUT", stdout) 70 t.Log("STDERR", stderr) 71 t.Fatalf("could not generate keypair") 72 } 73 74 return func() { 75 err := os.Unsetenv("COSIGN_PASSWORD") 76 if err != nil { 77 t.Fatal(err) 78 } 79 80 err = os.Remove("cosign.key") 81 if err != nil { 82 t.Fatalf("could not cleanup cosign.key") 83 } 84 85 err = os.Remove("cosign.pub") 86 if err != nil { 87 t.Fatalf("could not cleanup cosign.key") 88 } 89 } 90 } 91 92 func getFixtureImage(t testing.TB, fixtureImageName string) string { 93 t.Logf("obtaining fixture image for %s", fixtureImageName) 94 imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) 95 return imagetest.GetFixtureImageTarPath(t, fixtureImageName) 96 } 97 98 func pullDockerImage(t testing.TB, image string) { 99 cmd := exec.Command("docker", "pull", image) 100 stdout, stderr, _ := runCommand(cmd, nil) 101 if cmd.ProcessState.ExitCode() != 0 { 102 t.Log("STDOUT", stdout) 103 t.Log("STDERR", stderr) 104 t.Fatalf("could not pull docker image") 105 } 106 } 107 108 // docker run -v $(pwd)/sbom:/sbom cyclonedx/cyclonedx-cli:latest validate --input-format json --input-version v1_4 --input-file /sbom 109 func runCycloneDXInDocker(t testing.TB, env map[string]string, image string, f *os.File, args ...string) (*exec.Cmd, string, string) { 110 allArgs := append( 111 []string{ 112 "run", 113 "-t", 114 "-v", 115 fmt.Sprintf("%s:/sbom", f.Name()), 116 image, 117 }, 118 args..., 119 ) 120 cmd := exec.Command("docker", allArgs...) 121 stdout, stderr, _ := runCommand(cmd, env) 122 return cmd, stdout, stderr 123 } 124 125 func runBuildxInDocker(t testing.TB, env map[string]string, image string, args ...string) (*exec.Cmd, string, string) { 126 allArgs := append( 127 []string{ 128 "run", 129 "-t", 130 "-e", 131 "BUILDX_CHECK_FOR_APP_UPDATE=false", 132 "-v", 133 fmt.Sprintf("%s:/buildx", getBuildxBinaryLocationByOS(t, "linux")), 134 image, 135 "/buildx", 136 }, 137 args..., 138 ) 139 cmd := exec.Command("docker", allArgs...) 140 stdout, stderr, _ := runCommand(cmd, env) 141 return cmd, stdout, stderr 142 } 143 144 func runBuildx(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) { 145 return runBuildxCommand(t, env, true, args...) 146 } 147 148 func runBuildxSafe(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) { 149 return runBuildxCommand(t, env, false, args...) 150 } 151 152 func runBuildxCommand(t testing.TB, env map[string]string, expectError bool, args ...string) (*exec.Cmd, string, string) { 153 cancel := make(chan bool, 1) 154 defer func() { 155 cancel <- true 156 }() 157 158 cmd := getBuildxCommand(t, args...) 159 if env == nil { 160 env = make(map[string]string) 161 } 162 163 // we should not have tests reaching out for app update checks 164 env["BUILDX_CHECK_FOR_APP_UPDATE"] = "false" 165 166 timeout := func() { 167 select { 168 case <-cancel: 169 return 170 case <-time.After(60 * time.Second): 171 } 172 173 if cmd != nil && cmd.Process != nil { 174 // get a stack trace printed 175 err := cmd.Process.Signal(syscall.SIGABRT) 176 if err != nil { 177 t.Errorf("error aborting: %+v", err) 178 } 179 } 180 } 181 182 go timeout() 183 184 stdout, stderr, err := runCommand(cmd, env) 185 186 if !expectError && err != nil && stdout == "" { 187 t.Errorf("error running buildx: %+v", err) 188 t.Errorf("STDOUT: %s", stdout) 189 t.Errorf("STDERR: %s", stderr) 190 191 // this probably indicates a timeout... lets run it again with more verbosity to help debug issues 192 args = append(args, "-vv") 193 cmd = getBuildxCommand(t, args...) 194 195 go timeout() 196 stdout, stderr, err = runCommand(cmd, env) 197 198 if err != nil { 199 t.Errorf("error rerunning buildx: %+v", err) 200 t.Errorf("STDOUT: %s", stdout) 201 t.Errorf("STDERR: %s", stderr) 202 } 203 } 204 205 return cmd, stdout, stderr 206 } 207 208 func runCommandObj(t testing.TB, cmd *exec.Cmd, env map[string]string, expectError bool) (string, string) { 209 cancel := make(chan bool, 1) 210 defer func() { 211 cancel <- true 212 }() 213 214 if env == nil { 215 env = make(map[string]string) 216 } 217 218 // we should not have tests reaching out for app update checks 219 env["BUILDX_CHECK_FOR_APP_UPDATE"] = "false" 220 221 timeout := func() { 222 select { 223 case <-cancel: 224 return 225 case <-time.After(60 * time.Second): 226 } 227 228 if cmd != nil && cmd.Process != nil { 229 // get a stack trace printed 230 err := cmd.Process.Signal(syscall.SIGABRT) 231 if err != nil { 232 t.Errorf("error aborting: %+v", err) 233 } 234 } 235 } 236 237 go timeout() 238 239 stdout, stderr, err := runCommand(cmd, env) 240 241 if !expectError && err != nil && stdout == "" { 242 t.Errorf("error running buildx: %+v", err) 243 t.Errorf("STDOUT: %s", stdout) 244 t.Errorf("STDERR: %s", stderr) 245 } 246 247 return stdout, stderr 248 } 249 250 func runCosign(t testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) { 251 cmd := getCommand(t, ".tmp/cosign", args...) 252 if env == nil { 253 env = make(map[string]string) 254 } 255 256 stdout, stderr, err := runCommand(cmd, env) 257 258 if err != nil { 259 t.Errorf("error running cosign: %+v", err) 260 } 261 262 return cmd, stdout, stderr 263 } 264 265 func getCommand(t testing.TB, location string, args ...string) *exec.Cmd { 266 return exec.Command(filepath.Join(repoRoot(t), location), args...) 267 } 268 269 func runCommand(cmd *exec.Cmd, env map[string]string) (string, string, error) { 270 if env != nil { 271 cmd.Env = append(os.Environ(), envMapToSlice(env)...) 272 } 273 var stdout, stderr bytes.Buffer 274 cmd.Stdout = &stdout 275 cmd.Stderr = &stderr 276 277 // ignore errors since this may be what the test expects 278 err := cmd.Run() 279 280 return stdout.String(), stderr.String(), err 281 } 282 283 func envMapToSlice(env map[string]string) (envList []string) { 284 for key, val := range env { 285 if key == "" { 286 continue 287 } 288 envList = append(envList, fmt.Sprintf("%s=%s", key, val)) 289 } 290 return 291 } 292 293 func getBuildxCommand(t testing.TB, args ...string) *exec.Cmd { 294 return exec.Command(getBuildxBinaryLocation(t), args...) 295 } 296 297 func getBuildxBinaryLocation(t testing.TB) string { 298 if os.Getenv("BUILDX_BINARY_LOCATION") != "" { 299 // BUILDX_BINARY_LOCATION is the absolute path to the snapshot binary 300 return os.Getenv("BUILDX_BINARY_LOCATION") 301 } 302 return getBuildxBinaryLocationByOS(t, runtime.GOOS) 303 } 304 305 func getBuildxBinaryLocationByOS(t testing.TB, goOS string) string { 306 // note: for amd64 we need to update the snapshot location with the v1 suffix 307 // see : https://goreleaser.com/customization/build/#why-is-there-a-_v1-suffix-on-amd64-builds 308 archPath := runtime.GOARCH 309 if runtime.GOARCH == "amd64" { 310 archPath = fmt.Sprintf("%s_v1", archPath) 311 } 312 // note: there is a subtle - vs _ difference between these versions 313 switch goOS { 314 case "darwin", "linux": 315 return path.Join(repoRoot(t), fmt.Sprintf("snapshot/%s-build_%s_%s/buildx", goOS, goOS, archPath)) 316 default: 317 t.Fatalf("unsupported OS: %s", runtime.GOOS) 318 } 319 return "" 320 } 321 322 func repoRoot(t testing.TB) string { 323 t.Helper() 324 root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() 325 if err != nil { 326 t.Fatalf("unable to find repo root dir: %+v", err) 327 } 328 absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root))) 329 if err != nil { 330 t.Fatal("unable to get abs path to repo root:", err) 331 } 332 return absRepoRoot 333 } 334 335 func testRetryIntervals(done <-chan struct{}) <-chan time.Duration { 336 return exponentialBackoffDurations(250*time.Millisecond, 4*time.Second, 2, done) 337 } 338 339 func exponentialBackoffDurations(minDuration, maxDuration time.Duration, step float64, done <-chan struct{}) <-chan time.Duration { 340 sleepDurations := make(chan time.Duration) 341 go func() { 342 defer close(sleepDurations) 343 retryLoop: 344 for attempt := 0; ; attempt++ { 345 duration := exponentialBackoffDuration(minDuration, maxDuration, step, attempt) 346 347 select { 348 case sleepDurations <- duration: 349 break 350 case <-done: 351 break retryLoop 352 } 353 354 if duration == maxDuration { 355 break 356 } 357 } 358 }() 359 return sleepDurations 360 } 361 362 func exponentialBackoffDuration(minDuration, maxDuration time.Duration, step float64, attempt int) time.Duration { 363 duration := time.Duration(float64(minDuration) * math.Pow(step, float64(attempt))) 364 if duration < minDuration { 365 return minDuration 366 } else if duration > maxDuration { 367 return maxDuration 368 } 369 return duration 370 }