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 }