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  }