github.com/quay/claircore@v1.5.28/test/bisect/main_test.go (about)

     1  package main_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"hash/fnv"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"os"
    13  	"os/exec"
    14  	"os/signal"
    15  	"path"
    16  	"path/filepath"
    17  	"sort"
    18  	"strings"
    19  	"testing"
    20  	"text/tabwriter"
    21  	"text/template"
    22  	"time"
    23  
    24  	"github.com/quay/zlog"
    25  	"github.com/rs/zerolog"
    26  	"golang.org/x/sync/errgroup"
    27  
    28  	"github.com/quay/claircore"
    29  	"github.com/quay/claircore/datastore/postgres"
    30  	"github.com/quay/claircore/libindex"
    31  	"github.com/quay/claircore/libvuln"
    32  	"github.com/quay/claircore/pkg/ctxlock"
    33  	"github.com/quay/claircore/test/integration"
    34  	_ "github.com/quay/claircore/updater/defaults"
    35  )
    36  
    37  var (
    38  	run          bool
    39  	stderr       bool
    40  	dumpManifest string
    41  	dumpIndex    string
    42  	dumpReport   string
    43  )
    44  
    45  var (
    46  	manifestFilename = template.New("manifest")
    47  	indexFilename    = template.New("index")
    48  	reportFilename   = template.New("report")
    49  )
    50  
    51  func TestMain(m *testing.M) {
    52  	var c int
    53  	defer func() { os.Exit(c) }()
    54  	flag.BoolVar(&run, "enable", false, "enable the bisect test")
    55  	flag.BoolVar(&stderr, "stderr", false, "dump logs to stderr")
    56  	flag.StringVar(&dumpManifest, "dump-manifest", "", "dump manifest to templated location, if provided")
    57  	flag.StringVar(&dumpIndex, "dump-index", "", "dump index to templated location, if provided")
    58  	flag.StringVar(&dumpReport, "dump-report", "", "dump report to templated location, if provided")
    59  	flag.Parse()
    60  	defer integration.DBSetup()()
    61  	c = m.Run()
    62  }
    63  
    64  func TestRun(t *testing.T) {
    65  	if !run {
    66  		t.Skip("skipping bisect tool run")
    67  	}
    68  	integration.NeedDB(t)
    69  	ctx := context.Background()
    70  	layersDir := integration.PackageCacheDir(t)
    71  	ctx, srv := setup(ctx, t, layersDir)
    72  
    73  	indexer := mkIndexer(ctx, t, srv.Client())
    74  	matcher := mkMatcher(ctx, t, srv.Client())
    75  	if err := waitForInit(ctx, matcher); err != nil {
    76  		t.Fatal(err)
    77  	}
    78  
    79  	var done context.CancelFunc
    80  	var tctx context.Context
    81  	if d, ok := t.Deadline(); ok {
    82  		tctx, done = context.WithDeadline(ctx, d.Add(-5*time.Second))
    83  	} else {
    84  		to := 20 * time.Minute
    85  		fmt.Fprintln(os.Stderr, "no timeout provided, setting to ", to)
    86  		tctx, done = context.WithTimeout(ctx, to)
    87  	}
    88  	defer done()
    89  	if flag.NArg() == 0 {
    90  		fmt.Fprintln(os.Stderr, `no images provided, running until timeout`)
    91  		<-tctx.Done()
    92  		return
    93  	}
    94  
    95  	eg, ctx := errgroup.WithContext(tctx)
    96  	for _, img := range flag.Args() {
    97  		eg.Go(runOne(ctx, indexer, matcher, layersDir, srv.URL, img))
    98  	}
    99  	if err := eg.Wait(); err != nil {
   100  		fmt.Fprintln(os.Stderr, err)
   101  		t.Error(err)
   102  	}
   103  }
   104  
   105  // RunOne the function returned runs an image "n" through the indexer and
   106  // matcher.
   107  //
   108  // The results are written in a text format to stdout.
   109  func runOne(ctx context.Context, indexer *libindex.Libindex, matcher *libvuln.Libvuln, root, url, n string) func() error {
   110  	h := fnv.New64a()
   111  	fmt.Fprint(h, n)
   112  	prefix := fmt.Sprintf("%x", h.Sum(nil))
   113  	workdir := filepath.Join(root, prefix)
   114  	return func() error {
   115  		var err error
   116  		_, stat := os.Stat(workdir)
   117  		if errors.Is(stat, os.ErrNotExist) {
   118  			args := []string{
   119  				`copy`, `--override-os`, `linux`, `--override-arch`, `amd64`, `docker://` + n, `dir:` + workdir,
   120  			}
   121  			cmd := exec.CommandContext(ctx, `skopeo`, args...)
   122  			err = cmd.Run()
   123  		}
   124  		if err != nil {
   125  			return err
   126  		}
   127  		f, err := os.Open(filepath.Join(workdir, `manifest.json`))
   128  		if err != nil {
   129  			return err
   130  		}
   131  		defer f.Close()
   132  		var rm regManifest
   133  		if err := json.NewDecoder(f).Decode(&rm); err != nil {
   134  			return err
   135  		}
   136  		m := rm.Manifest(url + `/` + prefix)
   137  		if err := writeOut(manifestFilename, n, &m); err != nil {
   138  			return err
   139  		}
   140  
   141  		ir, err := indexer.Index(ctx, &m)
   142  		if err != nil {
   143  			return err
   144  		}
   145  		if err := writeOut(indexFilename, n, ir); err != nil {
   146  			return err
   147  		}
   148  
   149  		vr, err := matcher.Scan(ctx, ir)
   150  		if err != nil {
   151  			return err
   152  		}
   153  		if err := writeOut(reportFilename, n, vr); err != nil {
   154  			return err
   155  		}
   156  
   157  		tw := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
   158  		defer tw.Flush()
   159  		pkgs := make([]string, 0, len(vr.Packages))
   160  		for pkg := range vr.Packages {
   161  			pkgs = append(pkgs, pkg)
   162  		}
   163  		sort.Strings(pkgs)
   164  		for _, pid := range pkgs {
   165  			vs, ok := vr.PackageVulnerabilities[pid]
   166  			if !ok {
   167  				continue
   168  			}
   169  			pkg := vr.Packages[pid]
   170  			for _, id := range vs {
   171  				v := vr.Vulnerabilities[id]
   172  				fmt.Fprintf(tw, "%s\t%s\t%s\n", n, pkg.Name, v.Name)
   173  			}
   174  		}
   175  		return nil
   176  	}
   177  }
   178  
   179  // RegManifest is a helper to go from a registry's manifest to a claircore
   180  // manifest.
   181  type regManifest struct {
   182  	Config struct {
   183  		Digest string `json:"digest"`
   184  	} `json:"config"`
   185  	Layers []regLayer `json:"layers"`
   186  }
   187  
   188  // Manifest returns a claircore Manifest derived from the regManifest, assuming
   189  // that layers can be downloaded by their digest if appended to "url".
   190  func (r *regManifest) Manifest(url string) (m claircore.Manifest) {
   191  	m.Hash = claircore.MustParseDigest(r.Config.Digest)
   192  	for _, l := range r.Layers {
   193  		m.Layers = append(m.Layers, &claircore.Layer{
   194  			Hash:    claircore.MustParseDigest(l.Digest),
   195  			URI:     url + `/` + l.Digest,
   196  			Headers: make(map[string][]string),
   197  		})
   198  	}
   199  	return m
   200  }
   201  
   202  // RegLayer is a helper to go from a registry's manifest to a claircore
   203  // manifest.
   204  type regLayer struct {
   205  	MediaType string `json:"mediaType"`
   206  	Digest    string `json:"digest"`
   207  	Size      int64  `json:"size"`
   208  }
   209  
   210  // EscapeImage makes a name safer for filesystem use.
   211  func escapeImage(i string) string {
   212  	i = strings.ReplaceAll(i, "/", "-")
   213  	i = strings.ReplaceAll(i, ":", "-")
   214  	return i
   215  }
   216  
   217  // Setup does a grip of test setup work, returning a context that will cancel on
   218  // interrupt and a server set up to serve files from "dir".
   219  func setup(ctx context.Context, t *testing.T, dir string) (context.Context, *httptest.Server) {
   220  	l := zerolog.Nop()
   221  	if stderr {
   222  		l = zerolog.New(zerolog.NewConsoleWriter())
   223  	}
   224  	zlog.Set(&l)
   225  
   226  	for _, v := range []struct {
   227  		Tmpl **template.Template
   228  		In   string
   229  	}{
   230  		{Tmpl: &manifestFilename, In: dumpManifest},
   231  		{Tmpl: &indexFilename, In: dumpIndex},
   232  		{Tmpl: &reportFilename, In: dumpReport},
   233  	} {
   234  		if v.In == "" {
   235  			*v.Tmpl = nil
   236  			continue
   237  		}
   238  		if _, err := (*v.Tmpl).Parse(v.In); err != nil {
   239  			t.Error(err)
   240  		}
   241  	}
   242  
   243  	ctx, done := signal.NotifyContext(ctx, os.Interrupt)
   244  	t.Cleanup(done)
   245  
   246  	if err := os.MkdirAll(dir, 0755); err != nil {
   247  		t.Fatal(err)
   248  	}
   249  	fsrv := http.FileServer(http.Dir(dir))
   250  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   251  		t.Log(r.URL.Path)
   252  		b := path.Base(r.URL.Path)
   253  		if strings.Contains(b, ":") {
   254  			n, err := r.URL.Parse(b[strings.IndexByte(b, ':')+1:])
   255  			if err != nil {
   256  				t.Logf("url weirdness: %v", err)
   257  			} else {
   258  				r.URL = n
   259  			}
   260  		}
   261  		fsrv.ServeHTTP(w, r)
   262  	}))
   263  	t.Cleanup(srv.Close)
   264  
   265  	return ctx, srv
   266  }
   267  
   268  // MkIndexer constructs an indexer and associates its cleanup with "t".
   269  func mkIndexer(ctx context.Context, t *testing.T, c *http.Client) *libindex.Libindex {
   270  	db := integration.NewPersistentDB(ctx, t, "indexer_bisect")
   271  	pool, err := postgres.Connect(ctx, db.String(), "indexer_bisect")
   272  	if err != nil {
   273  		t.Fatal(err)
   274  	}
   275  	store, err := postgres.InitPostgresIndexerStore(ctx, pool, true)
   276  	if err != nil {
   277  		t.Fatal(err)
   278  	}
   279  	locker, err := ctxlock.New(ctx, pool)
   280  	if err != nil {
   281  		t.Fatal(err)
   282  	}
   283  	fetcher := libindex.NewRemoteFetchArena(c, t.TempDir())
   284  	opts := libindex.Options{
   285  		Store:      store,
   286  		Locker:     locker,
   287  		FetchArena: fetcher,
   288  	}
   289  	i, err := libindex.New(ctx, &opts, c)
   290  	if err != nil {
   291  		db.Close(ctx, t)
   292  		t.Fatal(err)
   293  	}
   294  	t.Cleanup(func() {
   295  		db.Close(ctx, t)
   296  		if err := i.Close(ctx); err != nil && !errors.Is(err, context.Canceled) {
   297  			t.Error(err)
   298  		}
   299  	})
   300  	return i
   301  }
   302  
   303  // MkMatcher constructs a matcher and associates its cleanup with "t".
   304  func mkMatcher(ctx context.Context, t *testing.T, c *http.Client) *libvuln.Libvuln {
   305  	db := integration.NewPersistentDB(ctx, t, "matcher_bisect")
   306  	pool, err := postgres.Connect(ctx, db.String(), "matcher_bisect")
   307  	if err != nil {
   308  		t.Fatal(err)
   309  	}
   310  	store, err := postgres.InitPostgresMatcherStore(ctx, pool, true)
   311  	if err != nil {
   312  		t.Fatal(err)
   313  	}
   314  	locker, err := ctxlock.New(ctx, pool)
   315  	if err != nil {
   316  		t.Fatal(err)
   317  	}
   318  	opts := libvuln.Options{
   319  		Store:  store,
   320  		Locker: locker,
   321  		Client: c,
   322  	}
   323  	m, err := libvuln.New(ctx, &opts)
   324  	if err != nil {
   325  		db.Close(ctx, t)
   326  		t.Fatal(err)
   327  	}
   328  	t.Cleanup(func() {
   329  		db.Close(ctx, t)
   330  		if err := m.Close(ctx); err != nil && !errors.Is(err, context.Canceled) {
   331  			t.Error(err)
   332  		}
   333  	})
   334  	return m
   335  }
   336  
   337  // WaitForInit waits until the *Libvuln reports true for an Initialized call or
   338  // the passed Context times out.
   339  func waitForInit(ctx context.Context, m *libvuln.Libvuln) error {
   340  	timer := time.NewTicker(5 * time.Second)
   341  	defer timer.Stop()
   342  	for ok, err := m.Initialized(ctx); ; ok, err = m.Initialized(ctx) {
   343  		if err != nil {
   344  			return err
   345  		}
   346  		if ok {
   347  			break
   348  		}
   349  		fmt.Fprintln(os.Stderr, "waiting")
   350  		select {
   351  		case <-timer.C:
   352  			continue
   353  		case <-ctx.Done():
   354  			return err
   355  		}
   356  	}
   357  	return nil
   358  }
   359  
   360  // WriteOut runs the template "tmpl" with "name" as an input, then encodes "v"
   361  // as JSON and writes it into the file named by the template output.
   362  //
   363  // If "tmpl" is nil, the function returns nil immediately.
   364  func writeOut(tmpl *template.Template, name string, v interface{}) error {
   365  	if tmpl == nil {
   366  		return nil
   367  	}
   368  	var buf strings.Builder
   369  	if err := tmpl.Execute(&buf, escapeImage(name)); err != nil {
   370  		return err
   371  	}
   372  	f, err := os.Create(buf.String())
   373  	if err != nil {
   374  		return err
   375  	}
   376  	defer f.Close()
   377  	if err := json.NewEncoder(f).Encode(v); err != nil {
   378  		return err
   379  	}
   380  	return nil
   381  }