Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add remote.Descriptor.Schema1() #1626

Merged
merged 1 commit into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions pkg/v1/remote/descriptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
115 changes: 115 additions & 0 deletions pkg/v1/remote/schema1.go
Original file line number Diff line number Diff line change
@@ -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"`
}
134 changes: 134 additions & 0 deletions pkg/v1/remote/schema1_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
8 changes: 8 additions & 0 deletions pkg/v1/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}