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

     1  package test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/quay/claircore"
    15  	"github.com/quay/claircore/indexer"
    16  	"github.com/quay/claircore/internal/wart"
    17  	"github.com/quay/claircore/test/fetch"
    18  	"github.com/quay/claircore/test/integration"
    19  )
    20  
    21  // MediaType is a media type that can be used in [claircore.LayerDescription]s
    22  // in tests.
    23  const MediaType = `application/vnd.oci.image.layer.nondistributable.v1.tar`
    24  
    25  // CachedArena is an [indexer.FetchArena] that populates Layers out of
    26  // the testing caches.
    27  type CachedArena struct {
    28  	remote string
    29  	local  string
    30  }
    31  
    32  var _ indexer.FetchArena = (*CachedArena)(nil)
    33  
    34  // NewCachedArena returns an initialized CachedArena.
    35  func NewCachedArena(t testing.TB) *CachedArena {
    36  	return &CachedArena{
    37  		remote: filepath.Join(integration.CacheDir(t), "layer"),
    38  		local:  integration.PackageCacheDir(t),
    39  	}
    40  }
    41  
    42  // LoadLayerFromRegistry fetches a layer from a registry into the appropriate cache.
    43  func (a *CachedArena) LoadLayerFromRegistry(ctx context.Context, t testing.TB, ref LayerRef) {
    44  	t.Helper()
    45  	// Fetched layers are stored in the global cache.
    46  	d, err := claircore.ParseDigest(ref.Digest)
    47  	if err != nil {
    48  		t.Fatal(err)
    49  	}
    50  	_, err = fetch.Layer(ctx, t, ref.Registry, ref.Name, d)
    51  	if err != nil {
    52  		t.Fatal(err)
    53  	}
    54  }
    55  
    56  // LayerRef is a remote layer.
    57  type LayerRef struct {
    58  	Registry string
    59  	Name     string
    60  	Digest   string
    61  }
    62  
    63  // GenerateLayer is used for tests that generate their layer data rather than
    64  // fetch it from a registry.
    65  //
    66  // If the test fails, the cached file is removed. If successful, the layer can
    67  // be referenced by using a relative file URI for "name". That is, if the passed
    68  // name is "layer.tar", a [claircore.LayerDescription] should use a URI of
    69  // "file:layer.tar".
    70  //
    71  // It is the caller's responsibility to ensure that "name" is unique per-package.
    72  func (a *CachedArena) GenerateLayer(t testing.TB, name string, stamp time.Time, gen func(testing.TB, *os.File)) {
    73  	t.Helper()
    74  	GenerateFixture(t, name, stamp, gen)
    75  }
    76  
    77  // Realizer implements [indexer.FetchArena].
    78  func (a *CachedArena) Realizer(_ context.Context) indexer.Realizer {
    79  	return &CachedRealizer{
    80  		remote: a.remote,
    81  		local:  a.local,
    82  	}
    83  }
    84  
    85  // Close implements [indexer.FetchArena].
    86  func (a *CachedArena) Close(_ context.Context) error {
    87  	return nil
    88  }
    89  
    90  // CachedRealizer is the [indexer.Realizer] returned by [CachedArena].
    91  type CachedRealizer struct {
    92  	remote string
    93  	local  string
    94  	layers []claircore.Layer
    95  }
    96  
    97  var (
    98  	_ indexer.Realizer            = (*CachedRealizer)(nil)
    99  	_ indexer.DescriptionRealizer = (*CachedRealizer)(nil)
   100  )
   101  
   102  // RealizeDescriptions implements [indexer.DescriptionRealizer].
   103  func (r *CachedRealizer) RealizeDescriptions(ctx context.Context, descs []claircore.LayerDescription) ([]claircore.Layer, error) {
   104  	out := make([]claircore.Layer, len(descs))
   105  	var success bool
   106  	defer func() {
   107  		if success {
   108  			return
   109  		}
   110  		for i := range out {
   111  			if out[i].URI != "" {
   112  				out[i].Close()
   113  			}
   114  		}
   115  	}()
   116  
   117  	for i := range descs {
   118  		d := &descs[i]
   119  		var n string
   120  
   121  		u, err := url.Parse(d.URI)
   122  		if err != nil {
   123  			return nil, err
   124  		}
   125  		switch u.Scheme {
   126  		case "http", "https":
   127  			// Ignore the URI.
   128  			k, h, ok := strings.Cut(d.Digest, ":")
   129  			if !ok {
   130  				panic("invalid digest")
   131  			}
   132  			n = filepath.Join(r.remote, k, h)
   133  		case "file":
   134  			if u.Opaque == "" {
   135  				return nil, fmt.Errorf("bad URI: %v", u)
   136  			}
   137  			n = filepath.Join(r.local, u.Opaque)
   138  		default:
   139  			return nil, fmt.Errorf("unknown scheme: %q", u.Scheme)
   140  		}
   141  
   142  		f, err := os.Open(n)
   143  		if err != nil {
   144  			return nil, fmt.Errorf("unable to open %q: %v", n, err)
   145  		}
   146  		if err := out[i].Init(ctx, d, f); err != nil {
   147  			return nil, err
   148  		}
   149  	}
   150  
   151  	success = true
   152  	r.layers = out
   153  	return out, nil
   154  }
   155  
   156  // Realize implements [indexer.Realizer].
   157  func (r *CachedRealizer) Realize(ctx context.Context, ls []*claircore.Layer) error {
   158  	ds := wart.LayersToDescriptions(ls)
   159  	ret, err := r.RealizeDescriptions(ctx, ds)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	wart.CopyLayerPointers(ls, ret)
   164  	return nil
   165  }
   166  
   167  // Close implements [indexer.Realizer] and [indexer.DescriptionRealizer].
   168  func (r *CachedRealizer) Close() error {
   169  	errs := make([]error, len(r.layers))
   170  	for i := range r.layers {
   171  		errs[i] = r.layers[i].Close()
   172  	}
   173  	return errors.Join(errs...)
   174  }