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 }