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