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