From d9584448c2b570a6b9052b2ac060f43f1ebc03bd Mon Sep 17 00:00:00 2001 From: Jon Johnson Date: Thu, 6 Apr 2023 06:59:03 -0700 Subject: [PATCH] Add remote.Descriptor.Schema1() (#1626) This will allow just passing a remote.Descriptor to remote.Pusher and also gives an escape hatch for filesystem access to schema1 images. --- pkg/v1/remote/descriptor.go | 44 ++++++----- pkg/v1/remote/schema1.go | 115 +++++++++++++++++++++++++++++ pkg/v1/remote/schema1_test.go | 134 ++++++++++++++++++++++++++++++++++ pkg/v1/types/types.go | 8 ++ 4 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 pkg/v1/remote/schema1.go create mode 100644 pkg/v1/remote/schema1_test.go diff --git a/pkg/v1/remote/descriptor.go b/pkg/v1/remote/descriptor.go index 31247077d..d6e0199cc 100644 --- a/pkg/v1/remote/descriptor.go +++ b/pkg/v1/remote/descriptor.go @@ -36,6 +36,11 @@ import ( "github.com/google/go-containerregistry/pkg/v1/types" ) +var allManifestMediaTypes = append(append([]types.MediaType{ + types.DockerManifestSchema1, + types.DockerManifestSchema1Signed, +}, acceptableImageMediaTypes...), acceptableIndexMediaTypes...) + // ErrSchema1 indicates that we received a schema1 manifest from the registry. // This library doesn't have plans to support this legacy image format: // https://github.com/google/go-containerregistry/issues/377 @@ -79,14 +84,7 @@ func (d *Descriptor) RawManifest() ([]byte, error) { // // See Head if you don't need the response body. func Get(ref name.Reference, options ...Option) (*Descriptor, error) { - acceptable := []types.MediaType{ - // Just to look at them. - types.DockerManifestSchema1, - types.DockerManifestSchema1Signed, - } - acceptable = append(acceptable, acceptableImageMediaTypes...) - acceptable = append(acceptable, acceptableIndexMediaTypes...) - return get(ref, acceptable, options...) + return get(ref, allManifestMediaTypes, options...) } // Head returns a v1.Descriptor for the given reference by issuing a HEAD @@ -95,14 +93,6 @@ func Get(ref name.Reference, options ...Option) (*Descriptor, error) { // Note that the server response will not have a body, so any errors encountered // should be retried with Get to get more details. func Head(ref name.Reference, options ...Option) (*v1.Descriptor, error) { - acceptable := []types.MediaType{ - // Just to look at them. - types.DockerManifestSchema1, - types.DockerManifestSchema1Signed, - } - acceptable = append(acceptable, acceptableImageMediaTypes...) - acceptable = append(acceptable, acceptableIndexMediaTypes...) - o, err := makeOptions(options...) if err != nil { return nil, err @@ -113,7 +103,7 @@ func Head(ref name.Reference, options ...Option) (*v1.Descriptor, error) { return nil, err } - return f.headManifest(o.context, ref, acceptable) + return f.headManifest(o.context, ref, allManifestMediaTypes) } // Handle options and fetch the manifest with the acceptable MediaTypes in the @@ -177,6 +167,26 @@ func (d *Descriptor) Image() (v1.Image, error) { }, nil } +// Schema1 converts the Descriptor into a v1.Image for v2 schema 1 media types. +// +// The v1.Image returned by this method does not implement the entire interface because it would be inefficient. +// This exists mostly to make it easier to copy schema 1 images around or look at their filesystems. +// This is separate from Image() to avoid a backward incompatible change for callers expecting ErrSchema1. +func (d *Descriptor) Schema1() (v1.Image, error) { + i := &schema1{ + fetcher: d.fetcher, + ref: d.ref, + manifest: d.Manifest, + mediaType: d.MediaType, + descriptor: &d.Descriptor, + } + + return &mountableImage{ + Image: i, + Reference: d.ref, + }, nil +} + // ImageIndex converts the Descriptor into a v1.ImageIndex. func (d *Descriptor) ImageIndex() (v1.ImageIndex, error) { switch d.MediaType { diff --git a/pkg/v1/remote/schema1.go b/pkg/v1/remote/schema1.go new file mode 100644 index 000000000..96456a323 --- /dev/null +++ b/pkg/v1/remote/schema1.go @@ -0,0 +1,115 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "encoding/json" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type schema1 struct { + fetcher + ref name.Reference + manifest []byte + mediaType types.MediaType + descriptor *v1.Descriptor +} + +func (s *schema1) Layers() ([]v1.Layer, error) { + m := schema1Manifest{} + if err := json.NewDecoder(bytes.NewReader(s.manifest)).Decode(&m); err != nil { + return nil, err + } + + layers := []v1.Layer{} + for i := len(m.FSLayers) - 1; i >= 0; i-- { + fsl := m.FSLayers[i] + + h, err := v1.NewHash(fsl.BlobSum) + if err != nil { + return nil, err + } + l, err := s.LayerByDigest(h) + if err != nil { + return nil, err + } + layers = append(layers, l) + } + + return layers, nil +} + +func (s *schema1) MediaType() (types.MediaType, error) { + return s.mediaType, nil +} + +func (s *schema1) Size() (int64, error) { + return s.descriptor.Size, nil +} + +func (s *schema1) ConfigName() (v1.Hash, error) { + return partial.ConfigName(s) +} + +func (s *schema1) ConfigFile() (*v1.ConfigFile, error) { + return nil, newErrSchema1(s.mediaType) +} + +func (s *schema1) RawConfigFile() ([]byte, error) { + return []byte("{}"), nil +} + +func (s *schema1) Digest() (v1.Hash, error) { + return s.descriptor.Digest, nil +} + +func (s *schema1) Manifest() (*v1.Manifest, error) { + return nil, newErrSchema1(s.mediaType) +} + +func (s *schema1) RawManifest() ([]byte, error) { + return s.manifest, nil +} + +func (s *schema1) LayerByDigest(h v1.Hash) (v1.Layer, error) { + l, err := partial.CompressedToLayer(&remoteLayer{ + fetcher: s.fetcher, + digest: h, + }) + if err != nil { + return nil, err + } + return &MountableLayer{ + Layer: l, + Reference: s.ref.Context().Digest(h.String()), + }, nil +} + +func (s *schema1) LayerByDiffID(v1.Hash) (v1.Layer, error) { + return nil, newErrSchema1(s.mediaType) +} + +type fslayer struct { + BlobSum string `json:"blobSum"` +} + +type schema1Manifest struct { + FSLayers []fslayer `json:"fsLayers"` +} diff --git a/pkg/v1/remote/schema1_test.go b/pkg/v1/remote/schema1_test.go new file mode 100644 index 000000000..e8e4cd5d3 --- /dev/null +++ b/pkg/v1/remote/schema1_test.go @@ -0,0 +1,134 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +var fatal = log.Fatal +var helper = func() {} + +func must[T any](t T, err error) T { + helper() + if err != nil { + fatal(err) + } + return t +} + +type fakeSchema1 struct { + b []byte +} + +func (f *fakeSchema1) MediaType() (types.MediaType, error) { + return types.DockerManifestSchema1, nil +} + +func (f *fakeSchema1) RawManifest() ([]byte, error) { + return f.b, nil +} + +func toSchema1(t *testing.T, img v1.Image) *fakeSchema1 { + t.Helper() + + fsl := []fslayer{} + + layers := must(img.Layers()) + for i := len(layers) - 1; i >= 0; i-- { + l := layers[i] + dig := must(l.Digest()) + fsl = append(fsl, fslayer{ + BlobSum: dig.String(), + }) + } + + return &fakeSchema1{ + b: must(json.Marshal(&schema1Manifest{FSLayers: fsl})), + } +} + +func TestSchema1(t *testing.T) { + fatal = t.Fatal + helper = t.Helper + + rnd := must(random.Image(1024, 3)) + s1 := toSchema1(t, rnd) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u := must(url.Parse(s.URL)) + + dst := fmt.Sprintf("%s/test/foreign/upload", u.Host) + ref := must(name.ParseReference(dst)) + + if err := Write(ref, rnd); err != nil { + t.Fatal(err) + } + + tag := ref.Context().Tag("schema1") + + if err := Put(tag, s1); err != nil { + t.Fatal(err) + } + + pulled := must(Get(tag)) + img := must(pulled.Schema1()) + + if err := Write(ref.Context().Tag("repushed"), img); err != nil { + t.Fatal(err) + } + + mustErr := func(a any, err error) { + t.Helper() + if err == nil { + t.Fatalf("should have failed, got %T", a) + } + } + + mustErr(img.ConfigFile()) + mustErr(img.Manifest()) + mustErr(img.LayerByDiffID(v1.Hash{})) + + h, sz, err := v1.SHA256(bytes.NewReader(s1.b)) + if err != nil { + t.Fatal(err) + } + if got, want := must(img.Size()), sz; got != want { + t.Errorf("Size(): got %d, want %d", got, want) + } + if got, want := must(img.Digest()), h; got != want { + t.Errorf("Digest(): got %s, want %s", got, want) + } + + if got, want := must(io.ReadAll(mutate.Extract(img))), must(io.ReadAll(mutate.Extract(rnd))); !bytes.Equal(got, want) { + t.Error("filesystems are different") + } +} diff --git a/pkg/v1/types/types.go b/pkg/v1/types/types.go index efc6bd6b9..92a4492af 100644 --- a/pkg/v1/types/types.go +++ b/pkg/v1/types/types.go @@ -80,3 +80,11 @@ func (m MediaType) IsConfig() bool { } return false } + +func (m MediaType) IsSchema1() bool { + switch m { + case DockerManifestSchema1, DockerManifestSchema1Signed: + return true + } + return false +}