1
1
package oci
2
2
3
3
import (
4
+ "bytes"
4
5
"context"
5
6
"fmt"
7
+ "io"
6
8
"net"
7
9
"net/http"
8
10
"net/url"
@@ -18,6 +20,7 @@ import (
18
20
"github.com/buildbuddy-io/buildbuddy/server/util/tracing"
19
21
"github.com/distribution/reference"
20
22
"github.com/google/go-containerregistry/pkg/authn"
23
+ "github.com/google/go-containerregistry/pkg/v1/partial"
21
24
"github.com/google/go-containerregistry/pkg/v1/remote"
22
25
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
23
26
"github.com/google/go-containerregistry/pkg/v1/types"
33
36
defaultKeychainEnabled = flag .Bool ("executor.container_registry_default_keychain_enabled" , false , "Enable the default container registry keychain, respecting both docker configs and podman configs." )
34
37
allowedPrivateIPs = flag .Slice ("executor.container_registry_allowed_private_ips" , []string {}, "Allowed private IP ranges for container registries. Private IPs are disallowed by default." )
35
38
36
- writeManifestsToCache = flag .Bool ("executor.container_registry.write_manifests_to_cache" , false , "Write resolved manifests to the cache." )
39
+ writeManifestsToCache = flag .Bool ("executor.container_registry.write_manifests_to_cache" , false , "Write resolved manifests to the cache." )
40
+ readManifestsFromCache = flag .Bool ("executor.container_registry.read_manifests_from_cache" , false , "Read manifests from cache before fetching from upstream remote registry." )
41
+
42
+ defaultPlatform = gcr.Platform {
43
+ Architecture : "amd64" ,
44
+ OS : "linux" ,
45
+ }
37
46
)
38
47
39
48
type MirrorConfig struct {
@@ -230,15 +239,14 @@ func (r *Resolver) Resolve(ctx context.Context, imageName string, platform *rgpb
230
239
return nil , status .InvalidArgumentErrorf ("invalid image %q" , imageName )
231
240
}
232
241
242
+ gcrPlatform := gcr.Platform {
243
+ Architecture : platform .GetArch (),
244
+ OS : platform .GetOs (),
245
+ Variant : platform .GetVariant (),
246
+ }
233
247
remoteOpts := []remote.Option {
234
248
remote .WithContext (ctx ),
235
- remote .WithPlatform (
236
- gcr.Platform {
237
- Architecture : platform .GetArch (),
238
- OS : platform .GetOs (),
239
- Variant : platform .GetVariant (),
240
- },
241
- ),
249
+ remote .WithPlatform (gcrPlatform ),
242
250
}
243
251
if ! credentials .IsEmpty () {
244
252
remoteOpts = append (remoteOpts , remote .WithAuth (& authn.Basic {
@@ -253,37 +261,212 @@ func (r *Resolver) Resolve(ctx context.Context, imageName string, platform *rgpb
253
261
} else {
254
262
remoteOpts = append (remoteOpts , remote .WithTransport (tr ))
255
263
}
256
- remoteDesc , err := remote .Get (imageRef , remoteOpts ... )
264
+
265
+ manifestHash , rawManifest , fromCache , err := r .fetchRawManifestFromCacheOrRemote (ctx , imageRef , remoteOpts )
257
266
if err != nil {
258
- if t , ok := err .(* transport.Error ); ok && t .StatusCode == http .StatusUnauthorized {
259
- return nil , status .PermissionDeniedErrorf ("could not retrieve image manifest: %s" , err )
260
- }
261
- return nil , status .UnavailableErrorf ("could not retrieve manifest from remote: %s" , err )
267
+ return nil , err
268
+ }
269
+ manifest , err := gcr .ParseManifest (bytes .NewReader (rawManifest ))
270
+ if err != nil {
271
+ return nil , err
262
272
}
263
- if * writeManifestsToCache {
264
- contentType := string (remoteDesc .MediaType )
265
- err := ocicache .WriteManifestToAC (ctx , remoteDesc . Manifest , r .env .GetActionCacheClient (), imageRef , remoteDesc . Digest , contentType )
273
+ if ! fromCache && * writeManifestsToCache {
274
+ contentType := string (manifest .MediaType )
275
+ err := ocicache .WriteManifestToAC (ctx , rawManifest , r .env .GetActionCacheClient (), imageRef , * manifestHash , contentType )
266
276
if err != nil {
267
277
log .CtxWarningf (ctx , "Could not write manifest for %q to the cache: %s" , imageRef .Context (), err )
268
278
}
269
279
}
270
280
271
- // Image() should resolve both images and image indices to an appropriate image
272
- img , err := remoteDesc .Image ()
281
+ // Unless the manifest is an index (in which case, we need to fetch the appropriate manifest for the input platform below),
282
+ // return an Image constructed from this manifest.
283
+ if manifest .MediaType != types .OCIImageIndex && manifest .MediaType != types .DockerManifestList {
284
+ image := & imageFromManifest {
285
+ digest : imageRef .Context ().Digest (manifestHash .String ()),
286
+
287
+ rawManifest : rawManifest ,
288
+ manifest : manifest ,
289
+
290
+ remoteOpts : remoteOpts ,
291
+ }
292
+ return partial .CompressedToImage (image )
293
+ }
294
+
295
+ index , err := gcr .ParseIndexManifest (bytes .NewReader (rawManifest ))
273
296
if err != nil {
274
- switch remoteDesc .MediaType {
275
- // This is an "image index", a meta-manifest that contains a list of
276
- // {platform props, manifest hash} properties to allow client to decide
277
- // which manifest they want to use based on platform.
278
- case types .OCIImageIndex , types .DockerManifestList :
279
- return nil , status .UnknownErrorf ("could not get image in image index from descriptor: %s" , err )
280
- case types .OCIManifestSchema1 , types .DockerManifestSchema2 :
281
- return nil , status .UnknownErrorf ("could not get image from descriptor: %s" , err )
282
- default :
283
- return nil , status .UnknownErrorf ("descriptor has unknown media type %q, oci error: %s" , remoteDesc .MediaType , err )
297
+ return nil , err
298
+ }
299
+ var child * gcr.Descriptor
300
+ for _ , childDesc := range index .Manifests {
301
+ p := defaultPlatform
302
+ if childDesc .Platform != nil {
303
+ p = * childDesc .Platform
304
+ }
305
+
306
+ if matchesPlatform (p , gcrPlatform ) {
307
+ child = & childDesc
284
308
}
285
309
}
286
- return img , nil
310
+ if child == nil {
311
+ return nil , status .UnavailableErrorf ("no child with platform %+v for %s" , platform , imageRef .Context ())
312
+ }
313
+
314
+ childRef := imageRef .Context ().Digest (child .Digest .String ())
315
+ childHash , childRawManifest , childFromCache , err := r .fetchRawManifestFromCacheOrRemote (ctx , childRef , remoteOpts )
316
+ if err != nil {
317
+ return nil , err
318
+ }
319
+ childManifest , err := gcr .ParseManifest (bytes .NewReader (childRawManifest ))
320
+ if err != nil {
321
+ return nil , err
322
+ }
323
+ if ! childFromCache && * writeManifestsToCache {
324
+ childContentType := string (childManifest .MediaType )
325
+ err := ocicache .WriteManifestToAC (ctx , childRawManifest , r .env .GetActionCacheClient (), childRef , * childHash , childContentType )
326
+ if err != nil {
327
+ log .CtxWarningf (ctx , "Could not write manifest for %q to the cache: %s" , childRef .Context (), err )
328
+ }
329
+ }
330
+ if childManifest .MediaType == types .OCIImageIndex || childManifest .MediaType == types .DockerManifestList {
331
+ return nil , status .UnknownErrorf ("child manifest for %q is itself an image index" , childRef .Context ())
332
+ }
333
+
334
+ image := & imageFromManifest {
335
+ digest : childRef .Context ().Digest (childHash .String ()),
336
+
337
+ rawManifest : childRawManifest ,
338
+ manifest : childManifest ,
339
+
340
+ remoteOpts : remoteOpts ,
341
+ }
342
+ return partial .CompressedToImage (image )
343
+ }
344
+
345
+ func (r * Resolver ) fetchRawManifestFromCacheOrRemote (ctx context.Context , digestOrTagRef gcrname.Reference , remoteOpts []remote.Option ) (* gcr.Hash , []byte , bool , error ) {
346
+ headDesc , err := remote .Head (digestOrTagRef , remoteOpts ... )
347
+ if err != nil {
348
+ if t , ok := err .(* transport.Error ); ok && t .StatusCode == http .StatusUnauthorized {
349
+ return nil , nil , false , status .PermissionDeniedErrorf ("could not retrieve image manifest: %s" , err )
350
+ }
351
+ return nil , nil , false , status .UnavailableErrorf ("could not retrieve manifest from remote: %s" , err )
352
+ }
353
+
354
+ if * readManifestsFromCache {
355
+ mc , err := ocicache .FetchManifestFromAC (ctx , r .env .GetActionCacheClient (), digestOrTagRef , headDesc .Digest )
356
+ if err == nil {
357
+ return & headDesc .Digest , mc .Raw , true , nil
358
+ }
359
+ if ! status .IsNotFoundError (err ) {
360
+ log .CtxErrorf (ctx , "error fetching manifest %s from the CAS: %s" , digestOrTagRef .Context (), err )
361
+ }
362
+ }
363
+
364
+ manifestDigest := digestOrTagRef .Context ().Digest (headDesc .Digest .String ())
365
+ getDesc , err := remote .Get (manifestDigest , remoteOpts ... )
366
+ if err != nil {
367
+ if t , ok := err .(* transport.Error ); ok && t .StatusCode == http .StatusUnauthorized {
368
+ return nil , nil , false , status .PermissionDeniedErrorf ("could not retrieve image manifest: %s" , err )
369
+ }
370
+ return nil , nil , false , status .UnavailableErrorf ("could not retrieve manifest from remote: %s" , err )
371
+ }
372
+ return & getDesc .Digest , getDesc .Manifest , false , nil
373
+ }
374
+
375
+ type imageFromManifest struct {
376
+ digest gcrname.Digest
377
+
378
+ rawManifest []byte
379
+ manifest * gcr.Manifest
380
+
381
+ remoteOpts []remote.Option
382
+ }
383
+
384
+ func (i * imageFromManifest ) RawManifest () ([]byte , error ) {
385
+ return i .rawManifest , nil
386
+ }
387
+
388
+ // RawConfigFile returns the config file data from the parsed manifest if present,
389
+ // otherwise it fetches the config file from the upstream remote registry.
390
+ func (i * imageFromManifest ) RawConfigFile () ([]byte , error ) {
391
+ if i .manifest .Config .Data != nil {
392
+ return i .manifest .Config .Data , nil
393
+ }
394
+
395
+ configDigest := i .digest .Digest (i .manifest .Config .Digest .String ())
396
+ rl , err := remote .Layer (configDigest , i .remoteOpts ... )
397
+ if err != nil {
398
+ return nil , err
399
+ }
400
+ rc , err := rl .Uncompressed ()
401
+ if err != nil {
402
+ return nil , err
403
+ }
404
+ defer rc .Close ()
405
+ rawConfigFile , err := io .ReadAll (rc )
406
+ if err != nil {
407
+ return nil , err
408
+ }
409
+ return rawConfigFile , nil
410
+ }
411
+
412
+ func (i * imageFromManifest ) MediaType () (types.MediaType , error ) {
413
+ return i .manifest .MediaType , nil
414
+ }
415
+
416
+ // LayerByDigest fetches the specified layer from the upstream remote registry
417
+ // or returns a NotFoundError.
418
+ func (i * imageFromManifest ) LayerByDigest (hash gcr.Hash ) (partial.CompressedLayer , error ) {
419
+ for _ , d := range i .manifest .Layers {
420
+ if d .Digest == hash {
421
+ rlref := i .digest .Context ().Digest (hash .String ())
422
+ return remote .Layer (rlref , i .remoteOpts ... )
423
+ }
424
+ }
425
+ return nil , status .NotFoundErrorf ("could not find layer in image %q" , i .digest .Context ())
426
+ }
427
+
428
+ var _ partial.CompressedImageCore = (* imageFromManifest )(nil )
429
+
430
+ // matchesPlatform is taken from https://github.com/google/go-containerregistry/blob/v0.17.0/pkg/v1/remote/index.go
431
+ func matchesPlatform (given , required gcr.Platform ) bool {
432
+ // Required fields that must be identical.
433
+ if given .Architecture != required .Architecture || given .OS != required .OS {
434
+ return false
435
+ }
436
+
437
+ // Optional fields that may be empty, but must be identical if provided.
438
+ if required .OSVersion != "" && given .OSVersion != required .OSVersion {
439
+ return false
440
+ }
441
+ if required .Variant != "" && given .Variant != required .Variant {
442
+ return false
443
+ }
444
+
445
+ // Verify required platform's features are a subset of given platform's features.
446
+ if ! isSubset (given .OSFeatures , required .OSFeatures ) {
447
+ return false
448
+ }
449
+ if ! isSubset (given .Features , required .Features ) {
450
+ return false
451
+ }
452
+
453
+ return true
454
+ }
455
+
456
+ // isSubset is taken from https://github.com/google/go-containerregistry/blob/v0.17.0/pkg/v1/remote/index.go
457
+ func isSubset (lst , required []string ) bool {
458
+ set := make (map [string ]bool )
459
+ for _ , value := range lst {
460
+ set [value ] = true
461
+ }
462
+
463
+ for _ , value := range required {
464
+ if _ , ok := set [value ]; ! ok {
465
+ return false
466
+ }
467
+ }
468
+
469
+ return true
287
470
}
288
471
289
472
// RuntimePlatform returns the platform on which the program is being executed,
0 commit comments