Skip to content

Commit a971408

Browse files
authored
[oci resolver] read manifests from AC before fetching from upstream (#9380)
In `oci.Resolver.Resolve(...)`, attempt to read manifests from the AC before making a request to the upstream remote registry if the `executor.container_registry.read_manifests_from_cache` flag is set to `true`. My rollout plan for this change and #9374 is: * merge this change, * enable `executor.container_registry.write_manifests_to_cache` in dev (then prod), and * if there are no ill effects, enable `executor.container_registry.read_manifests_from_cache` in dev (then prod).
1 parent bbc0ff8 commit a971408

File tree

3 files changed

+412
-29
lines changed

3 files changed

+412
-29
lines changed

enterprise/server/util/oci/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ go_library(
2020
"@com_github_google_go_containerregistry//pkg/authn",
2121
"@com_github_google_go_containerregistry//pkg/name",
2222
"@com_github_google_go_containerregistry//pkg/v1:pkg",
23+
"@com_github_google_go_containerregistry//pkg/v1/partial",
2324
"@com_github_google_go_containerregistry//pkg/v1/remote",
2425
"@com_github_google_go_containerregistry//pkg/v1/remote/transport",
2526
"@com_github_google_go_containerregistry//pkg/v1/types",
@@ -33,12 +34,16 @@ go_test(
3334
srcs = ["oci_test.go"],
3435
deps = [
3536
":oci",
37+
"//enterprise/server/clientidentity",
3638
"//enterprise/server/remote_execution/platform",
3739
"//proto:registry_go_proto",
40+
"//server/interfaces",
41+
"//server/testutil/testcache",
3842
"//server/testutil/testenv",
3943
"//server/testutil/testfs",
4044
"//server/testutil/testregistry",
4145
"//server/util/proto",
46+
"//server/util/random",
4247
"//server/util/status",
4348
"//server/util/testing/flags",
4449
"@com_github_google_go_cmp//cmp",

enterprise/server/util/oci/oci.go

Lines changed: 212 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package oci
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
7+
"io"
68
"net"
79
"net/http"
810
"net/url"
@@ -18,6 +20,7 @@ import (
1820
"github.com/buildbuddy-io/buildbuddy/server/util/tracing"
1921
"github.com/distribution/reference"
2022
"github.com/google/go-containerregistry/pkg/authn"
23+
"github.com/google/go-containerregistry/pkg/v1/partial"
2124
"github.com/google/go-containerregistry/pkg/v1/remote"
2225
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
2326
"github.com/google/go-containerregistry/pkg/v1/types"
@@ -33,7 +36,13 @@ var (
3336
defaultKeychainEnabled = flag.Bool("executor.container_registry_default_keychain_enabled", false, "Enable the default container registry keychain, respecting both docker configs and podman configs.")
3437
allowedPrivateIPs = flag.Slice("executor.container_registry_allowed_private_ips", []string{}, "Allowed private IP ranges for container registries. Private IPs are disallowed by default.")
3538

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+
}
3746
)
3847

3948
type MirrorConfig struct {
@@ -230,15 +239,14 @@ func (r *Resolver) Resolve(ctx context.Context, imageName string, platform *rgpb
230239
return nil, status.InvalidArgumentErrorf("invalid image %q", imageName)
231240
}
232241

242+
gcrPlatform := gcr.Platform{
243+
Architecture: platform.GetArch(),
244+
OS: platform.GetOs(),
245+
Variant: platform.GetVariant(),
246+
}
233247
remoteOpts := []remote.Option{
234248
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),
242250
}
243251
if !credentials.IsEmpty() {
244252
remoteOpts = append(remoteOpts, remote.WithAuth(&authn.Basic{
@@ -253,37 +261,212 @@ func (r *Resolver) Resolve(ctx context.Context, imageName string, platform *rgpb
253261
} else {
254262
remoteOpts = append(remoteOpts, remote.WithTransport(tr))
255263
}
256-
remoteDesc, err := remote.Get(imageRef, remoteOpts...)
264+
265+
manifestHash, rawManifest, fromCache, err := r.fetchRawManifestFromCacheOrRemote(ctx, imageRef, remoteOpts)
257266
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
262272
}
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)
266276
if err != nil {
267277
log.CtxWarningf(ctx, "Could not write manifest for %q to the cache: %s", imageRef.Context(), err)
268278
}
269279
}
270280

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))
273296
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
284308
}
285309
}
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
287470
}
288471

289472
// RuntimePlatform returns the platform on which the program is being executed,

0 commit comments

Comments
 (0)