diff --git a/pkg/sif/image.go b/pkg/sif/image.go index fcd5542..e78d0d2 100644 --- a/pkg/sif/image.go +++ b/pkg/sif/image.go @@ -46,6 +46,11 @@ func (im *image) RawManifest() ([]byte, error) { return im.rawManifest, nil } +// Descriptor returns the original descriptor from an index manifest. See partial.Descriptor. +func (im *image) Descriptor() (*v1.Descriptor, error) { + return im.desc, nil +} + var errLayerNotFoundInImage = errors.New("layer not found in image") // LayerByDigest returns a Layer for interacting with a particular layer of the image, looking it diff --git a/pkg/sif/image_test.go b/pkg/sif/image_test.go index ddbe26e..9636aee 100644 --- a/pkg/sif/image_test.go +++ b/pkg/sif/image_test.go @@ -5,46 +5,79 @@ package sif_test import ( + "reflect" "testing" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/validate" + "github.com/sylabs/oci-tools/pkg/sif" ) -func TestFile_Image(t *testing.T) { +// imageIndexFromPath returns an ImageIndex for the test to use, populated from the OCI Image +// Layout with the specified path in the corpus. +func imageIndexFromPath(t *testing.T, path string) v1.ImageIndex { + t.Helper() + + ii, err := sif.ImageIndexFromFileImage(fileImageFromPath(t, path)) + if err != nil { + t.Fatal(err) + } + + return ii +} + +func Test_imageIndex_Image(t *testing.T) { + imageDigest := v1.Hash{ + Algorithm: "sha256", + Hex: "432f982638b3aefab73cc58ab28f5c16e96fdb504e8c134fc58dff4bae8bf338", + } + imageDescriptor := &v1.Descriptor{ + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Size: 525, + Digest: imageDigest, + Platform: &v1.Platform{ + Architecture: "arm64", + OS: "linux", + Variant: "v8", + }, + } + tests := []struct { - name string - path string - hash v1.Hash + name string + ii v1.ImageIndex + hash v1.Hash + wantErr bool + wantDescriptor *v1.Descriptor }{ { - name: "DockerManifest", - path: corpus.SIF(t, "hello-world-docker-v2-manifest"), - hash: v1.Hash{ - Algorithm: "sha256", - Hex: "432f982638b3aefab73cc58ab28f5c16e96fdb504e8c134fc58dff4bae8bf338", - }, + name: "DockerManifest", + ii: imageIndexFromPath(t, "hello-world-docker-v2-manifest"), + hash: imageDigest, + wantDescriptor: imageDescriptor, }, { - name: "DockerManifestList", - path: corpus.SIF(t, "hello-world-docker-v2-manifest-list"), - hash: v1.Hash{ - Algorithm: "sha256", - Hex: "432f982638b3aefab73cc58ab28f5c16e96fdb504e8c134fc58dff4bae8bf338", - }, + name: "DockerManifestList", + ii: imageIndexFromPath(t, "hello-world-docker-v2-manifest-list"), + hash: imageDigest, + wantDescriptor: imageDescriptor, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ii := imageIndexFromPath(t, tt.path) - - img, err := ii.Image(tt.hash) + img, err := tt.ii.Image(tt.hash) if err != nil { t.Fatal(err) } if err := validate.Image(img); err != nil { - t.Fatal(err) + t.Error(err) + } + + if d, err := partial.Descriptor(img); err != nil { + t.Error(err) + } else if got, want := d, tt.wantDescriptor; !reflect.DeepEqual(got, want) { + t.Errorf("got descriptor %+v, want %+v", got, want) } }) } diff --git a/pkg/sif/index.go b/pkg/sif/index.go index 6407868..8124a1a 100644 --- a/pkg/sif/index.go +++ b/pkg/sif/index.go @@ -5,6 +5,7 @@ package sif import ( + "bytes" "encoding/json" "errors" "fmt" @@ -26,7 +27,7 @@ func ImageIndexFromFileImage(fi *sif.FileImage) (v1.ImageIndex, error) { type imageIndex struct { f *fileImage - mediaType types.MediaType + desc *v1.Descriptor rawManifest []byte } @@ -44,26 +45,35 @@ func (f *fileImage) ImageIndex() (v1.ImageIndex, error) { return nil, err } + digest, size, err := v1.SHA256(bytes.NewReader(b)) + if err != nil { + return nil, err + } + return &imageIndex{ - f: f, - mediaType: types.OCIImageIndex, + f: f, + desc: &v1.Descriptor{ + MediaType: types.OCIImageIndex, + Size: size, + Digest: digest, + }, rawManifest: b, }, nil } // MediaType of this image's manifest. func (ix *imageIndex) MediaType() (types.MediaType, error) { - return ix.mediaType, nil + return ix.desc.MediaType, nil } // Digest returns the sha256 of this index's manifest. func (ix *imageIndex) Digest() (v1.Hash, error) { - return partial.Digest(ix) + return ix.desc.Digest, nil } // Size returns the size of the manifest. func (ix *imageIndex) Size() (int64, error) { - return partial.Size(ix) + return ix.desc.Size, nil } // IndexManifest returns this image index's manifest object. @@ -78,6 +88,11 @@ func (ix *imageIndex) RawManifest() ([]byte, error) { return ix.rawManifest, nil } +// Descriptor returns the original descriptor from an index manifest. See partial.Descriptor. +func (ix *imageIndex) Descriptor() (*v1.Descriptor, error) { + return ix.desc, nil +} + var errUnexpectedMediaType = errors.New("unexpected media type") // Image returns a v1.Image that this ImageIndex references. @@ -96,12 +111,12 @@ func (ix *imageIndex) Image(h v1.Hash) (v1.Image, error) { return nil, err } - img := &image{ + img := image{ f: ix.f, desc: desc, rawManifest: b, } - return partial.CompressedToImage(img) + return partial.CompressedToImage(&img) } // ImageIndex returns a v1.ImageIndex that this ImageIndex references. @@ -122,8 +137,8 @@ func (ix *imageIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { return &imageIndex{ f: ix.f, + desc: desc, rawManifest: b, - mediaType: desc.MediaType, }, nil } diff --git a/pkg/sif/index_test.go b/pkg/sif/index_test.go index cb264e5..6b4b451 100644 --- a/pkg/sif/index_test.go +++ b/pkg/sif/index_test.go @@ -5,6 +5,7 @@ package sif_test import ( + "reflect" "testing" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -13,43 +14,71 @@ import ( ssif "github.com/sylabs/sif/v2/pkg/sif" ) -func imageIndexFromPath(t *testing.T, path string) v1.ImageIndex { +type withDescriptor interface { + Descriptor() (*v1.Descriptor, error) +} + +// fileImageFromPath returns a temporary FileImage for the test to use, populated from the OCI +// Image Layout with the specified path in the corpus. The FileImage is automatically unloaded when +// the test and all its subtests complete. +func fileImageFromPath(t *testing.T, path string) *ssif.FileImage { t.Helper() - f, err := ssif.LoadContainerFromPath(path) + f, err := ssif.LoadContainerFromPath(corpus.SIF(t, path)) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = f.UnloadContainer() }) - ii, err := sif.ImageIndexFromFileImage(f) - if err != nil { - t.Fatal(err) - } - - return ii + return f } -func TestFile_ImageIndex(t *testing.T) { +func TestImageIndexFromFileImage(t *testing.T) { tests := []struct { - name string - path string + name string + f *ssif.FileImage + wantDescriptor *v1.Descriptor }{ { name: "DockerManifest", - path: corpus.SIF(t, "hello-world-docker-v2-manifest"), + f: fileImageFromPath(t, "hello-world-docker-v2-manifest"), + wantDescriptor: &v1.Descriptor{ + MediaType: "application/vnd.oci.image.index.v1+json", + Size: 314, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "a2ca8d2eb29d4b32cabd3f2ca67c14c8ae178b93c3000da3ec63faca49a688e4", + }, + }, }, { name: "DockerManifestList", - path: corpus.SIF(t, "hello-world-docker-v2-manifest-list"), + f: fileImageFromPath(t, "hello-world-docker-v2-manifest-list"), + wantDescriptor: &v1.Descriptor{ + MediaType: "application/vnd.oci.image.index.v1+json", + Size: 2069, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "00e1ee7c898a2c393ea2fe7680938f8dcbe55e51fbf08032cf37326a677f92ed", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ii := imageIndexFromPath(t, tt.path) + ii, err := sif.ImageIndexFromFileImage(tt.f) + if err != nil { + t.Fatal(err) + } if err := validate.Index(ii); err != nil { - t.Fatal(err) + t.Error(err) + } + + if d, err := ii.(withDescriptor).Descriptor(); err != nil { + t.Error(err) + } else if got, want := d, tt.wantDescriptor; !reflect.DeepEqual(got, want) { + t.Errorf("got descriptor %+v, want %+v", got, want) } }) } diff --git a/pkg/sif/layer.go b/pkg/sif/layer.go index f86a8ca..8ce6d11 100644 --- a/pkg/sif/layer.go +++ b/pkg/sif/layer.go @@ -39,7 +39,7 @@ func (l *layer) MediaType() (types.MediaType, error) { return l.desc.MediaType, nil } -// Descriptor implements partial.withDescriptor. +// Descriptor returns the original descriptor from an image manifest. See partial.Descriptor. func (l *layer) Descriptor() (*v1.Descriptor, error) { return &l.desc, nil } diff --git a/pkg/sif/layer_test.go b/pkg/sif/layer_test.go new file mode 100644 index 0000000..856a6cd --- /dev/null +++ b/pkg/sif/layer_test.go @@ -0,0 +1,76 @@ +// Copyright 2023 Sylabs Inc. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + +package sif_test + +import ( + "reflect" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" +) + +// layerFromPath returns a Layer for the test to use, populated from the OCI Image with the +// specified path/digests in the corpus. +func layerFromPath(t *testing.T, path string, imageDigest, layerDigest string) v1.Layer { + t.Helper() + + ii := imageIndexFromPath(t, path) + + imageHash, err := v1.NewHash(imageDigest) + if err != nil { + t.Fatal(err) + } + + img, err := ii.Image(imageHash) + if err != nil { + t.Fatal(err) + } + + layerHash, err := v1.NewHash(layerDigest) + if err != nil { + t.Fatal(err) + } + + l, err := img.LayerByDigest(layerHash) + if err != nil { + t.Fatal(err) + } + + return l +} + +func TestLayer_Descriptor(t *testing.T) { + tests := []struct { + name string + l v1.Layer + wantDescriptor *v1.Descriptor + }{ + { + name: "DockerManifest", + l: layerFromPath(t, "hello-world-docker-v2-manifest", + "sha256:432f982638b3aefab73cc58ab28f5c16e96fdb504e8c134fc58dff4bae8bf338", + "sha256:7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01", + ), + wantDescriptor: &v1.Descriptor{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 3208, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "7050e35b49f5e348c4809f5eff915842962cb813f32062d3bbdd35c750dd7d01", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if d, err := partial.Descriptor(tt.l); err != nil { + t.Error(err) + } else if got, want := d, tt.wantDescriptor; !reflect.DeepEqual(got, want) { + t.Errorf("got descriptor %+v, want %+v", got, want) + } + }) + } +}