diff --git a/cnb_image.go b/cnb_image.go index af8c78a8..9822141c 100644 --- a/cnb_image.go +++ b/cnb_image.go @@ -10,6 +10,7 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/go-containerregistry/pkg/v1/validate" ) @@ -22,10 +23,13 @@ type CNBImageCore struct { // required v1.Image // the working image // optional - createdAt time.Time - preferredMediaTypes MediaTypes - preserveHistory bool - previousImage v1.Image + createdAt time.Time + preferredMediaTypes MediaTypes + preserveHistory bool + previousImage v1.Image + os, arch, variant, osVersion string + features, osFeatures, urls []string + annotations map[string]string } var _ v1.Image = &CNBImageCore{} @@ -34,6 +38,10 @@ var _ v1.Image = &CNBImageCore{} // TBD Deprecated: Architecture func (i *CNBImageCore) Architecture() (string, error) { + if i.arch != "" { + return i.arch, nil + } + configFile, err := getConfigFile(i.Image) if err != nil { return "", err @@ -127,6 +135,10 @@ func (i *CNBImageCore) ManifestSize() (int64, error) { // TBD Deprecated: OS func (i *CNBImageCore) OS() (string, error) { + if i.os != "" { + return i.os, nil + } + configFile, err := getConfigFile(i.Image) if err != nil { return "", err @@ -136,6 +148,10 @@ func (i *CNBImageCore) OS() (string, error) { // TBD Deprecated: OSVersion func (i *CNBImageCore) OSVersion() (string, error) { + if i.osVersion != "" { + return i.osVersion, nil + } + configFile, err := getConfigFile(i.Image) if err != nil { return "", err @@ -143,6 +159,67 @@ func (i *CNBImageCore) OSVersion() (string, error) { return configFile.OSVersion, nil } +func (i *CNBImageCore) OSFeatures() ([]string, error) { + if len(i.osFeatures) != 0 { + return i.osFeatures, nil + } + + configFile, err := getConfigFile(i.Image) + if err != nil { + return nil, err + } + return configFile.OSFeatures, nil +} + +func (i *CNBImageCore) Features() ([]string, error) { + if len(i.features) != 0 { + return i.features, nil + } + + mfest, err := getManifest(i.Image) + if err != nil { + return nil, err + } + + p := mfest.Config.Platform + if p == nil || len(p.Features) < 1 { + return nil, ErrFeaturesUndefined(i.preferredMediaTypes.ManifestType(), "") + } + return p.Features, nil +} + +func (i *CNBImageCore) URLs() ([]string, error) { + if len(i.urls) != 0 { + return i.urls, nil + } + + mfest, err := getManifest(i.Image) + if err != nil { + return nil, err + } + + if len(mfest.Config.URLs) < 1 { + return nil, ErrURLsUndefined(i.preferredMediaTypes.ManifestType(), "") + } + return mfest.Config.URLs, nil +} + +func (i *CNBImageCore) Annotations() (map[string]string, error) { + if len(i.annotations) != 0 { + return i.annotations, nil + } + + mfest, err := getManifest(i.Image) + if err != nil { + return nil, err + } + + if len(mfest.Annotations) < 1 { + return nil, ErrAnnotationsUndefined(i.preferredMediaTypes.ManifestType(), "") + } + return mfest.Annotations, nil +} + func (i *CNBImageCore) TopLayer() (string, error) { layers, err := i.Image.Layers() if err != nil { @@ -171,6 +248,10 @@ func (i *CNBImageCore) Valid() bool { // TBD Deprecated: Variant func (i *CNBImageCore) Variant() (string, error) { + if i.variant != "" { + return i.variant, nil + } + configFile, err := getConfigFile(i.Image) if err != nil { return "", err @@ -207,6 +288,7 @@ func (i *CNBImageCore) AnnotateRefName(refName string) error { // TBD Deprecated: SetArchitecture func (i *CNBImageCore) SetArchitecture(architecture string) error { + i.arch = architecture return i.MutateConfigFile(func(c *v1.ConfigFile) { c.Architecture = architecture }) @@ -266,6 +348,7 @@ func (i *CNBImageCore) SetLabel(key, val string) error { } func (i *CNBImageCore) SetOS(osVal string) error { + i.os = osVal return i.MutateConfigFile(func(c *v1.ConfigFile) { c.OS = osVal }) @@ -273,18 +356,56 @@ func (i *CNBImageCore) SetOS(osVal string) error { // TBD Deprecated: SetOSVersion func (i *CNBImageCore) SetOSVersion(osVersion string) error { + i.osVersion = osVersion return i.MutateConfigFile(func(c *v1.ConfigFile) { c.OSVersion = osVersion }) } +func (i *CNBImageCore) SetOSFeatures(osFeatures []string) error { + i.osFeatures = append(i.osFeatures, osFeatures...) + return i.MutateConfigFile(func(c *v1.ConfigFile) { + c.OSFeatures = osFeatures + }) +} + +func (i *CNBImageCore) SetFeatures(features []string) (err error) { + i.features = append(i.features, features...) + return nil +} + +func (i *CNBImageCore) SetURLs(urls []string) (err error) { + i.urls = append(i.urls, urls...) + return nil +} + +func (i *CNBImageCore) SetAnnotations(annotations map[string]string) error { + if len(i.annotations) == 0 { + i.annotations = make(map[string]string) + } + + for k, v := range annotations { + i.annotations[k] = v + } + return nil +} + // TBD Deprecated: SetVariant func (i *CNBImageCore) SetVariant(variant string) error { + i.variant = variant return i.MutateConfigFile(func(c *v1.ConfigFile) { c.Variant = variant }) } +func (i *CNBImageCore) Digest() (v1.Hash, error) { + return i.Image.Digest() +} + +func (i *CNBImageCore) MediaType() (types.MediaType, error) { + return i.Image.MediaType() +} + // TBD Deprecated: SetWorkingDir func (i *CNBImageCore) SetWorkingDir(dir string) error { return i.MutateConfigFile(func(c *v1.ConfigFile) { @@ -502,7 +623,7 @@ func getConfigFile(image v1.Image) (*v1.ConfigFile, error) { return nil, err } if configFile == nil { - return nil, errors.New("missing config file") + return nil, ErrConfigFileUndefined } return configFile, nil } @@ -513,7 +634,7 @@ func getManifest(image v1.Image) (*v1.Manifest, error) { return nil, err } if manifest == nil { - return nil, errors.New("missing manifest") + return nil, ErrManifestUndefined } return manifest, nil } diff --git a/fakes/image.go b/fakes/image.go index abf27712..9a38c221 100644 --- a/fakes/image.go +++ b/fakes/image.go @@ -2,8 +2,10 @@ package fakes import ( "archive/tar" + "bytes" "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "io" "os" @@ -13,6 +15,10 @@ import ( registryName "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" "github.com/pkg/errors" "github.com/buildpacks/imgutil" @@ -37,32 +43,274 @@ func NewImage(name, topLayerSha string, identifier imgutil.Identifier) *Image { } } +var ERRLayerNotFound = errors.New("layer with given diff id not found") + type Image struct { - deleted bool - layers []string - history []v1.History - layersMap map[string]string - prevLayersMap map[string]string - reusedLayers []string - labels map[string]string - env map[string]string - topLayerSha string - os string - osVersion string - architecture string - variant string - identifier imgutil.Identifier - name string - entryPoint []string - cmd []string - base string - createdAt time.Time - layerDir string - workingDir string - savedNames map[string]bool - manifestSize int64 - refName string - savedAnnotations map[string]string + deleted bool + layers []string + history []v1.History + layersMap map[string]string + prevLayersMap map[string]string + reusedLayers []string + labels map[string]string + env map[string]string + topLayerSha string + os string + osVersion string + architecture string + variant string + identifier imgutil.Identifier + name string + entryPoint []string + cmd []string + base string + createdAt time.Time + layerDir string + workingDir string + savedNames map[string]bool + manifestSize int64 + refName string + savedAnnotations map[string]string + features, osFeatures, urls []string +} + +func mapToStringSlice(data map[string]string) []string { + var stringSlice []string + for key, value := range data { + keyValue := fmt.Sprintf("%s=%s", key, value) + stringSlice = append(stringSlice, keyValue) + } + return stringSlice +} + +// ConfigFile implements v1.Image. +func (i *Image) ConfigFile() (*v1.ConfigFile, error) { + var hashes = make([]v1.Hash, 0) + + for _, layer := range i.layers { + hash, err := v1.NewHash(layer) + if err != nil { + return nil, err + } + + hashes = append(hashes, hash) + } + return &v1.ConfigFile{ + Architecture: i.architecture, + OS: i.os, + OSVersion: i.osVersion, + Variant: i.variant, + OSFeatures: i.osFeatures, + History: i.history, + Created: v1.Time{Time: i.createdAt}, + Author: "buildpacks", + Container: "containerd", + DockerVersion: "25.0", + RootFS: v1.RootFS{ + DiffIDs: hashes, + }, + Config: v1.Config{ + Cmd: i.cmd, + Env: mapToStringSlice(i.env), + ArgsEscaped: true, + Image: i.identifier.String(), + WorkingDir: i.workingDir, + Labels: i.labels, + User: "cnb", + }, + }, nil +} + +// ConfigName implements v1.Image. +func (i *Image) ConfigName() (v1.Hash, error) { + c, err := i.ConfigFile() + if err != nil { + return v1.Hash{}, err + } + + return v1.NewHash(c.Config.Image) +} + +// LayerByDiffID implements v1.Image. +func (i *Image) LayerByDiffID(hash v1.Hash) (v1.Layer, error) { + c, err := i.ConfigFile() + if err != nil { + return nil, err + } + + for _, diffId := range c.RootFS.DiffIDs { + if hash == diffId { + return Layer(1024, types.DockerLayer, WithHash(hash)) + } + } + + return nil, ERRLayerNotFound +} + +// LayerByDigest implements v1.Image. +func (i *Image) LayerByDigest(hash v1.Hash) (v1.Layer, error) { + for _, layer := range i.layers { + if h, err := v1.NewHash(layer); err == nil { + return Layer(1024, types.DockerLayer, WithHash(h)) + } + } + + return nil, ERRLayerNotFound +} + +// Layers implements v1.Image. +func (i *Image) Layers() (layers []v1.Layer, err error) { + for _, layer := range i.layers { + hash, err := v1.NewHash(layer) + if err != nil { + return nil, err + } + + l, err := Layer(1024, types.DockerLayer, WithHash(hash)) + if err != nil { + return layers, err + } + layers = append(layers, l) + } + + return layers, err +} + +type FakeConfigFile struct { + v1.ConfigFile +} + +func NewFakeConfigFile(config v1.ConfigFile) FakeConfigFile { + return FakeConfigFile{ + ConfigFile: config, + } +} + +func (c FakeConfigFile) RawManifest() ([]byte, error) { + return json.Marshal(c.ConfigFile) +} + +type FakeManifest struct { + v1.Manifest +} + +func NewFakeManifest(mfest v1.Manifest) FakeManifest { + return FakeManifest{ + Manifest: mfest, + } +} + +func (c FakeManifest) RawManifest() ([]byte, error) { + return json.Marshal(c.Manifest) +} + +func (i *Image) ConfigFileToV1Desc(config v1.ConfigFile) (desc v1.Descriptor, err error) { + fakeConfig := NewFakeConfigFile(config) + size, err := partial.Size(fakeConfig) + if err != nil { + return desc, err + } + + digest, err := partial.Digest(fakeConfig) + if err != nil { + return desc, err + } + + return v1.Descriptor{ + MediaType: types.DockerConfigJSON, + Size: size, + Digest: digest, + URLs: i.urls, + Annotations: i.savedAnnotations, + Platform: &v1.Platform{ + OS: i.os, + Architecture: i.architecture, + Variant: i.variant, + OSVersion: i.osVersion, + Features: i.features, + OSFeatures: i.osFeatures, + }, + }, nil +} + +// Manifest implements v1.Image. +func (i *Image) Manifest() (*v1.Manifest, error) { + layers, err := i.Layers() + if err != nil { + return nil, err + } + + var layerDesc = make([]v1.Descriptor, 0) + for _, layer := range layers { + desc := v1.Descriptor{} + if desc.Digest, err = layer.Digest(); err != nil { + return nil, err + } + + if desc.MediaType, err = layer.MediaType(); err != nil { + return nil, err + } + + if desc.Size, err = layer.Size(); err != nil { + return nil, err + } + + layerDesc = append(layerDesc, desc) + } + + cfgFile, err := i.ConfigFile() + if err != nil { + return nil, err + } + + configDesc, err := i.ConfigFileToV1Desc(*cfgFile) + if err != nil { + return nil, err + } + + manifest := &v1.Manifest{ + SchemaVersion: 1, + MediaType: types.DockerManifestList, + Layers: layerDesc, + Config: configDesc, + Subject: &configDesc, + Annotations: i.savedAnnotations, + } + + return manifest, nil +} + +// RawConfigFile implements v1.Image. +func (i *Image) RawConfigFile() ([]byte, error) { + config, err := i.ConfigFile() + if err != nil { + return nil, err + } + + return json.Marshal(config) +} + +// RawManifest implements v1.Image. +func (i *Image) RawManifest() ([]byte, error) { + mfest, err := i.Manifest() + if err != nil { + return nil, err + } + + return json.Marshal(mfest) +} + +// Size implements v1.Image. +func (i *Image) Size() (int64, error) { + mfest, err := i.Manifest() + if err != nil { + return 0, err + } + if mfest == nil { + return 0, imgutil.ErrManifestUndefined + } + + return partial.Size(NewFakeManifest(*mfest)) } func (i *Image) CreatedAt() (time.Time, error) { @@ -101,6 +349,22 @@ func (i *Image) Variant() (string, error) { return i.variant, nil } +func (i *Image) Features() ([]string, error) { + return i.features, nil +} + +func (i *Image) OSFeatures() ([]string, error) { + return i.osFeatures, nil +} + +func (i *Image) URLs() ([]string, error) { + return i.urls, nil +} + +func (i *Image) Annotations() (map[string]string, error) { + return i.savedAnnotations, nil +} + func (i *Image) Rename(name string) { i.name = name } @@ -113,6 +377,14 @@ func (i *Image) Identifier() (imgutil.Identifier, error) { return i.identifier, nil } +func (i *Image) Digest() (v1.Hash, error) { + return v1.Hash{}, nil +} + +func (i *Image) MediaType() (types.MediaType, error) { + return types.MediaType(""), nil +} + func (i *Image) Kind() string { return "" } @@ -169,6 +441,32 @@ func (i *Image) SetVariant(a string) error { return nil } +func (i *Image) SetFeatures(features []string) error { + i.features = append(i.features, features...) + return nil +} + +func (i *Image) SetOSFeatures(osFeatures []string) error { + i.osFeatures = append(i.osFeatures, osFeatures...) + return nil +} + +func (i *Image) SetURLs(urls []string) error { + i.urls = append(i.urls, urls...) + return nil +} + +func (i *Image) SetAnnotations(annos map[string]string) error { + if len(i.savedAnnotations) < 1 { + i.savedAnnotations = make(map[string]string) + } + + for k, v := range annos { + i.savedAnnotations[k] = v + } + return nil +} + func (i *Image) SetWorkingDir(dir string) error { i.workingDir = dir return nil @@ -501,3 +799,48 @@ func (i *Image) ManifestSize() (int64, error) { func (i *Image) SavedAnnotations() map[string]string { return i.savedAnnotations } + +// uncompressedLayer implements partial.UncompressedLayer from raw bytes. +type uncompressedLayer struct { + diffID v1.Hash + mediaType types.MediaType + content []byte +} + +// DiffID implements partial.UncompressedLayer +func (ul *uncompressedLayer) DiffID() (v1.Hash, error) { + return ul.diffID, nil +} + +// Uncompressed implements partial.UncompressedLayer +func (ul *uncompressedLayer) Uncompressed() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(ul.content)), nil +} + +// MediaType returns the media type of the layer +func (ul *uncompressedLayer) MediaType() (types.MediaType, error) { + return ul.mediaType, nil +} + +var _ partial.UncompressedLayer = (*uncompressedLayer)(nil) + +// Image returns a pseudo-randomly generated Image. +func V1Image(byteSize, layers int64, options ...Option) (v1.Image, error) { + adds := make([]mutate.Addendum, 0, 5) + for i := int64(0); i < layers; i++ { + layer, err := Layer(byteSize, types.DockerLayer, options...) + if err != nil { + return nil, err + } + adds = append(adds, mutate.Addendum{ + Layer: layer, + History: v1.History{ + Author: "random.Image", + Comment: fmt.Sprintf("this is a random history %d of %d", i, layers), + CreatedBy: "random", + }, + }) + } + + return mutate.Append(empty.Image, adds...) +} diff --git a/fakes/index.go b/fakes/index.go new file mode 100644 index 00000000..df13189a --- /dev/null +++ b/fakes/index.go @@ -0,0 +1,1091 @@ +package fakes + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "runtime" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/pkg/errors" + + "github.com/buildpacks/imgutil" +) + +func NewIndex(format types.MediaType, byteSize, layers, count int64, desc v1.Descriptor, ops ...Option) (*Index, error) { + var ( + annotate = make(map[v1.Hash]v1.Descriptor, 0) + images = make(map[v1.Hash]v1.Image, 0) + ) + + idx, err := ImageIndex(byteSize, layers, count, desc, ops...) + if err != nil { + return nil, err + } + + mfest, err := idx.IndexManifest() + if err != nil { + return nil, err + } + + if mfest == nil { + mfest = &v1.IndexManifest{} + } + + for _, m := range mfest.Manifests { + img, err := idx.Image(m.Digest) + if err != nil { + return nil, err + } + + images[m.Digest] = img + + config, err := img.ConfigFile() + if err != nil { + return nil, err + } + + if config == nil { + config = &v1.ConfigFile{} + } + + imgMfest, err := img.Manifest() + if err != nil { + return nil, err + } + + if imgMfest == nil { + imgMfest = &v1.Manifest{} + } + + platform := imgMfest.Config.Platform + + if platform == nil && imgMfest.Subject != nil && imgMfest.Subject.Platform != nil { + platform = imgMfest.Subject.Platform + } + + if platform == nil { + platform = &v1.Platform{} + } + + annotate[m.Digest] = v1.Descriptor{ + Platform: &v1.Platform{ + OS: config.OS, + Architecture: config.Architecture, + Variant: config.Variant, + OSVersion: config.OSVersion, + Features: platform.Features, + OSFeatures: config.OSFeatures, + }, + Annotations: imgMfest.Annotations, + URLs: imgMfest.Config.URLs, + } + } + + return &Index{ + ImageIndex: idx, + format: format, + byteSize: byteSize, + layers: layers, + count: count, + ops: ops, + Annotate: annotate, + images: images, + }, nil +} + +func computeIndex(idx *Index) error { + mfest, err := idx.IndexManifest() + if err != nil { + return err + } + + if mfest == nil { + mfest = &v1.IndexManifest{} + } + + for _, m := range mfest.Manifests { + img, err := idx.Image(m.Digest) + if err != nil { + return err + } + + idx.images[m.Digest] = img + + config, err := img.ConfigFile() + if err != nil { + return err + } + + if config == nil { + config = &v1.ConfigFile{} + } + + imgMfest, err := img.Manifest() + if err != nil { + return err + } + + if imgMfest == nil { + imgMfest = &v1.Manifest{} + } + + platform := imgMfest.Config.Platform + + if platform == nil && imgMfest.Subject != nil && imgMfest.Subject.Platform != nil { + platform = imgMfest.Subject.Platform + } + + if platform == nil { + platform = &v1.Platform{} + } + + idx.Annotate[m.Digest] = v1.Descriptor{ + Platform: &v1.Platform{ + OS: config.OS, + Architecture: config.Architecture, + OSVersion: config.OSVersion, + OSFeatures: config.OSFeatures, + Variant: config.Variant, + Features: platform.Features, + }, + Annotations: imgMfest.Annotations, + URLs: imgMfest.Config.URLs, + } + } + return nil +} + +var _ imgutil.ImageIndex = (*Index)(nil) + +type Index struct { + Annotate map[v1.Hash]v1.Descriptor + format types.MediaType + byteSize, layers, count int64 + ops []Option + isDeleted, shouldSave, AddIndex bool + images map[v1.Hash]v1.Image + v1.ImageIndex +} + +func (i *Index) compute() { + for h, v := range i.Annotate { + i.ImageIndex = mutate.AppendManifests(i.ImageIndex, mutate.IndexAddendum{ + Add: i.images[h], + Descriptor: v, + }) + } +} + +func (i *Index) OS(digest name.Digest) (os string, err error) { + i.compute() + if i.isDeleted { + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.Platform.OS, nil + } + + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (i *Index) Architecture(digest name.Digest) (arch string, err error) { + i.compute() + if i.isDeleted { + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest("") + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.Platform.Architecture, nil + } + + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest("") +} + +func (i *Index) Variant(digest name.Digest) (osVariant string, err error) { + i.compute() + if i.isDeleted { + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.Platform.Variant, nil + } + + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (i *Index) OSVersion(digest name.Digest) (osVersion string, err error) { + i.compute() + if i.isDeleted { + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.Platform.OSVersion, nil + } + + return "", imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (i *Index) Features(digest name.Digest) (features []string, err error) { + i.compute() + if i.isDeleted { + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.Platform.Features, nil + } + + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (i *Index) OSFeatures(digest name.Digest) (osFeatures []string, err error) { + i.compute() + if i.isDeleted { + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.Platform.OSFeatures, nil + } + + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (i *Index) Annotations(digest name.Digest) (annotations map[string]string, err error) { + i.compute() + if i.isDeleted { + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest("") + } + + if i.format == types.DockerManifestList { + return nil, nil + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.Annotations, nil + } + + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest(hash.String()) +} + +func (i *Index) URLs(digest name.Digest) (urls []string, err error) { + i.compute() + if i.isDeleted { + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if desc, ok := i.Annotate[hash]; ok { + return desc.URLs, nil + } + + return nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (i *Index) SetOS(digest name.Digest, os string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Platform.OS = os + i.Annotate[hash] = desc + return nil +} + +func (i *Index) SetArchitecture(digest name.Digest, arch string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + platform := desc.Platform + platform.Architecture = arch + desc.Platform = platform + i.Annotate[hash] = desc + return nil +} + +func (i *Index) SetVariant(digest name.Digest, osVariant string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + platform := desc.Platform + platform.Variant = osVariant + desc.Platform = platform + i.Annotate[hash] = desc + return nil +} + +func (i *Index) SetOSVersion(digest name.Digest, osVersion string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + platform := desc.Platform + platform.OSVersion = osVersion + desc.Platform = platform + i.Annotate[hash] = desc + return nil +} + +func (i *Index) SetFeatures(digest name.Digest, features []string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + platform := desc.Platform + platform.Features = append(desc.Platform.Features, features...) + desc.Platform = platform + i.Annotate[hash] = desc + return nil +} + +func (i *Index) SetOSFeatures(digest name.Digest, osFeatures []string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + platform := desc.Platform + platform.OSFeatures = append(desc.Platform.OSFeatures, osFeatures...) + desc.Platform = platform + i.Annotate[hash] = desc + return nil +} + +func (i *Index) SetAnnotations(digest name.Digest, annotations map[string]string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + if len(desc.Annotations) == 0 { + desc.Annotations = make(map[string]string, 0) + } + + for k, v := range annotations { + desc.Annotations[k] = v + } + i.Annotate[hash] = desc + return nil +} + +func (i *Index) SetURLs(digest name.Digest, urls []string) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + if _, err := i.OS(digest); err != nil { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + i.shouldSave = true + desc := i.Annotate[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.URLs = append(desc.URLs, urls...) + i.Annotate[hash] = desc + return nil +} + +func (i *Index) Add(ref name.Reference, ops ...imgutil.IndexAddOption) error { + hash, err := v1.NewHash(ref.Identifier()) + if err != nil { + length := 4 + b := make([]byte, length) + hash, _, err = v1.SHA256(strings.NewReader(string(b))) + if err != nil { + return err + } + } + + addOps := &imgutil.AddOptions{} + for _, op := range ops { + op(addOps) + } + + desc := func(format types.MediaType) v1.Descriptor { + return v1.Descriptor{ + Digest: hash, + MediaType: format, + Annotations: addOps.Annotations, + Platform: &v1.Platform{ + OS: addOps.OS, + Architecture: addOps.Arch, + OSVersion: addOps.OSVersion, + Variant: addOps.Variant, + Features: addOps.Features, + OSFeatures: addOps.OSFeatures, + }, + } + } + + if idx, ok := i.ImageIndex.(*randomIndex); ok { + if i.AddIndex { + if i.format == types.DockerManifestList { + imgs, err := idx.addIndex(hash, types.DockerManifestSchema2, i.byteSize, i.layers, i.count, *addOps) + if err != nil { + return err + } + + for _, img := range imgs { + err := i.addImage(img, desc(types.DockerManifestSchema2)) + if err != nil { + return err + } + } + } + imgs, err := idx.addIndex(hash, types.OCIManifestSchema1, i.byteSize, i.layers, i.count, *addOps) + if err != nil { + return err + } + + for _, img := range imgs { + err := i.addImage(img, desc(types.OCIManifestSchema1)) + if err != nil { + return err + } + } + } + if i.format == types.DockerManifestList { + img, err := idx.addImage(hash, types.DockerManifestSchema2, i.byteSize, i.layers, i.count, *addOps) + if err != nil { + return err + } + + return i.addImage(img, desc(types.DockerManifestSchema2)) + } + img, err := idx.addImage(hash, types.OCIManifestSchema1, i.byteSize, i.layers, i.count, *addOps) + if err != nil { + return err + } + + return i.addImage(img, desc(types.OCIManifestSchema1)) + } + + return errors.New("index is not random index") +} + +func (i *Index) addImage(image v1.Image, desc v1.Descriptor) error { + i.shouldSave = true + if err := satisifyPlatform(image, &desc); err != nil { + return err + } + + if config, err := configFromDesc(image, desc); err == nil { + image, err = mutate.ConfigFile(image, config) + if err != nil { + return err + } + } + + image = mutate.Annotations(image, desc.Annotations).(v1.Image) + image = mutate.Subject(image, desc).(v1.Image) + i.ImageIndex = mutate.AppendManifests(i.ImageIndex, mutate.IndexAddendum{ + Add: image, + Descriptor: desc, + }) + return computeIndex(i) +} + +func configFromDesc(image v1.Image, desc v1.Descriptor) (*v1.ConfigFile, error) { + config, err := image.ConfigFile() + if err != nil { + return nil, err + } + + if config == nil { + return nil, imgutil.ErrConfigFileUndefined + } + + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + p := desc.Platform + if p == nil { + return config, nil + } + + if p.OS != "" { + config.OS = p.OS + } + + if p.Architecture != "" { + config.Architecture = p.Architecture + } + + if p.Variant != "" { + config.Variant = p.Variant + } + + if p.OSVersion != "" { + config.OSVersion = p.OSVersion + } + + if len(p.Features) != 0 { + plat := config.Platform() + if plat == nil { + plat = &v1.Platform{} + } + + plat.Features = append(plat.Features, p.Features...) + } + + if len(p.OSFeatures) != 0 { + config.OSFeatures = append(config.OSFeatures, p.OSFeatures...) + } + + return config, nil +} + +func satisifyPlatform(image v1.Image, desc *v1.Descriptor) error { + config, err := image.ConfigFile() + if err != nil { + return err + } + + if config == nil { + return imgutil.ErrConfigFileUndefined + } + + mfest, err := image.Manifest() + if err != nil { + return err + } + + if mfest == nil { + return imgutil.ErrManifestUndefined + } + + features := make([]string, 0) + if p := config.Platform(); p != nil { + features = p.Features + } + + platform := &v1.Platform{ + OS: config.OS, + Architecture: config.Architecture, + Variant: config.Variant, + OSVersion: config.OSVersion, + Features: features, + OSFeatures: config.OSFeatures, + } + + if p := desc.Platform; !p.Equals(*platform) { + switch { + case p.OS != "": + platform.OS = p.OS + fallthrough + case p.Architecture != "": + platform.Architecture = p.Architecture + fallthrough + case p.Variant != "": + platform.Variant = p.Variant + fallthrough + case p.OSVersion != "": + platform.OSVersion = p.OSVersion + fallthrough + case len(p.Features) != 0: + platform.Features = append(platform.Features, p.Features...) + fallthrough + case len(p.OSFeatures) != 0: + platform.OSFeatures = append(platform.OSFeatures, p.OSFeatures...) + } + } + + annos := make(map[string]string) + if len(mfest.Annotations) != 0 { + annos = mfest.Annotations + } + + for k, v := range desc.Annotations { + annos[k] = v + } + + desc.Annotations = annos + desc.Platform = platform + return nil +} + +func (i *Index) Save() error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest("") + } + + i.shouldSave = false + return nil +} + +func (i *Index) Push(ops ...imgutil.IndexPushOption) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest("") + } + + if i.shouldSave { + return errors.New("index should need to be saved") + } + + return nil +} + +func (i *Index) Inspect() (mfestStr string, err error) { + i.compute() + if i.isDeleted { + return mfestStr, imgutil.ErrNoImageOrIndexFoundWithGivenDigest("") + } + + if i.shouldSave { + return mfestStr, errors.New("index should need to be saved") + } + + mfest, err := i.ImageIndex.IndexManifest() + if err != nil { + return mfestStr, err + } + + if mfest == nil { + return mfestStr, imgutil.ErrManifestUndefined + } + + mfestBytes, err := json.MarshalIndent(mfest, "", " ") + if err != nil { + return mfestStr, err + } + + return string(mfestBytes), nil +} + +func (i *Index) Remove(digest name.Reference) error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) + } + + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return err + } + + delete(i.images, hash) + delete(i.Annotate, hash) + return nil +} + +func (i *Index) Delete() error { + if i.isDeleted { + return imgutil.ErrNoImageOrIndexFoundWithGivenDigest("") + } + + i.isDeleted = true + i.shouldSave = false + return nil +} + +type randomIndex struct { + images map[v1.Hash]v1.Image + indexes map[v1.Hash]v1.ImageIndex + manifest *v1.IndexManifest +} + +// Index returns a pseudo-randomly generated ImageIndex with count images, each +// having the given number of layers of size byteSize. +func ImageIndex(byteSize, layers, count int64, desc v1.Descriptor, options ...Option) (v1.ImageIndex, error) { + manifest := v1.IndexManifest{ + SchemaVersion: 2, + MediaType: types.OCIImageIndex, + Manifests: []v1.Descriptor{}, + Subject: &desc, + } + + images := make(map[v1.Hash]v1.Image) + + indexes := make(map[v1.Hash]v1.ImageIndex) + withouIndex := WithIndex(false) + o := getOptions(options) + + if o.withIndex { + withouIndex(o) + options = append(options, WithSource(o.source), WithIndex(o.withIndex)) + for i := int64(0); i < count; i++ { + idx, err := ImageIndex(byteSize, layers, count, desc, options...) + if err != nil { + return nil, err + } + + rawManifest, err := idx.RawManifest() + if err != nil { + return nil, err + } + digest, size, err := v1.SHA256(bytes.NewReader(rawManifest)) + if err != nil { + return nil, err + } + mediaType, err := idx.MediaType() + if err != nil { + return nil, err + } + + manifest.Manifests = append(manifest.Manifests, v1.Descriptor{ + Digest: digest, + Size: size, + MediaType: mediaType, + }) + + indexes[digest] = idx + } + } else { + for i := int64(0); i < count; i++ { + img, err := V1Image(byteSize, layers, options...) + if err != nil { + return nil, err + } + + rawManifest, err := img.RawManifest() + if err != nil { + return nil, err + } + digest, size, err := v1.SHA256(bytes.NewReader(rawManifest)) + if err != nil { + return nil, err + } + mediaType, err := img.MediaType() + if err != nil { + return nil, err + } + + manifest.Manifests = append(manifest.Manifests, v1.Descriptor{ + Digest: digest, + Size: size, + MediaType: mediaType, + }) + + images[digest] = img + } + } + + return &randomIndex{ + images: images, + indexes: indexes, + manifest: &manifest, + }, nil +} + +func (i *randomIndex) MediaType() (types.MediaType, error) { + return i.manifest.MediaType, nil +} + +func (i *randomIndex) Digest() (v1.Hash, error) { + return partial.Digest(i) +} + +func (i *randomIndex) Size() (int64, error) { + return partial.Size(i) +} + +func (i *randomIndex) IndexManifest() (*v1.IndexManifest, error) { + return i.manifest, nil +} + +func (i *randomIndex) RawManifest() ([]byte, error) { + m, err := i.IndexManifest() + if err != nil { + return nil, err + } + return json.Marshal(m) +} + +func (i *randomIndex) Image(h v1.Hash) (v1.Image, error) { + if img, ok := i.images[h]; ok { + return img, nil + } + + return nil, fmt.Errorf("image not found: %v", h) +} + +func (i *randomIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + if idx, ok := i.indexes[h]; ok { + return idx, nil + } + + return nil, fmt.Errorf("image not found: %v", h) +} + +func (i *randomIndex) addImage(hash v1.Hash, format types.MediaType, byteSize, layers, _ int64, options imgutil.AddOptions) (v1.Image, error) { + img, err := V1Image(byteSize, layers) + if err != nil { + return img, err + } + + rawManifest, err := img.RawManifest() + if err != nil { + return img, err + } + _, size, err := v1.SHA256(bytes.NewReader(rawManifest)) + if err != nil { + return img, err + } + + i.manifest.Manifests = append(i.manifest.Manifests, v1.Descriptor{ + Digest: hash, + Size: size, + MediaType: format, + Annotations: options.Annotations, + Platform: &v1.Platform{ + OS: options.OS, + Architecture: options.Arch, + Variant: options.Variant, + OSVersion: options.OSVersion, + Features: options.Features, + OSFeatures: options.OSFeatures, + }, + }) + + i.images[hash] = img + + return img, nil +} + +func randStr() (string, error) { + length := 10 // adjust the length as needed + + b := make([]byte, length) + _, err := rand.Read(b) // read random bytes + if err != nil { + fmt.Println("Error generating random bytes:", err) + return "", err + } + + return base64.URLEncoding.EncodeToString(b)[:length], nil +} + +func (i *randomIndex) addIndex(hash v1.Hash, format types.MediaType, byteSize, layers, _ int64, ops imgutil.AddOptions) ([]v1.Image, error) { + switch { + case ops.All: + var images = make([]v1.Image, 0) + for _, v := range AllPlatforms { + str, err := randStr() + if err != nil { + return nil, err + } + + d, _, err := v1.SHA256(bytes.NewReader([]byte(str))) + if err != nil { + return nil, err + } + + img, err := i.addImage(d, format, byteSize, layers, 1, imgutil.AddOptions{ + OS: v.OS, + Arch: v.Arch, + Variant: v.Variant, + }) + if err != nil { + return nil, err + } + + images = append(images, img) + } + + return images, nil + case ops.OS != "", + ops.Arch != "", + ops.Variant != "", + ops.OSVersion != "", + len(ops.Features) != 0, + len(ops.OSFeatures) != 0, + len(ops.Annotations) != 0: + img, err := i.addImage(hash, format, byteSize, layers, 1, ops) + return []v1.Image{img}, err + default: + img, err := i.addImage(hash, format, byteSize, layers, 1, imgutil.AddOptions{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + }) + return []v1.Image{img}, err + } +} + +type Platform struct { + OS string `json:"os"` + Arch string `json:"arch"` + Variant string `json:"variant,omitempty"` // Optional variant field +} + +var AllPlatforms = map[string]Platform{ + "linux/amd64": {OS: "linux", Arch: "amd64"}, + "linux/arm64": {OS: "linux", Arch: "arm64"}, + "linux/386": {OS: "linux", Arch: "386"}, + "linux/mips64": {OS: "linux", Arch: "mips64"}, + "linux/mipsle": {OS: "linux", Arch: "mipsle"}, + "linux/ppc64le": {OS: "linux", Arch: "ppc64le"}, + "linux/s390x": {OS: "linux", Arch: "s390x"}, + "darwin/amd64": {OS: "darwin", Arch: "amd64"}, + "darwin/arm64": {OS: "darwin", Arch: "arm64"}, + "windows/amd64": {OS: "windows", Arch: "amd64"}, + "windows/386": {OS: "windows", Arch: "386"}, + "freebsd/amd64": {OS: "freebsd", Arch: "amd64"}, + "freebsd/386": {OS: "freebsd", Arch: "386"}, + "netbsd/amd64": {OS: "netbsd", Arch: "amd64"}, + "netbsd/386": {OS: "netbsd", Arch: "386"}, + "openbsd/amd64": {OS: "openbsd", Arch: "amd64"}, + "openbsd/386": {OS: "openbsd", Arch: "386"}, + "dragonfly/amd64": {OS: "dragonfly", Arch: "amd64"}, + "dragonfly/386": {OS: "dragonfly", Arch: "386"}, +} diff --git a/fakes/index_test.go b/fakes/index_test.go new file mode 100644 index 00000000..c24f65bb --- /dev/null +++ b/fakes/index_test.go @@ -0,0 +1,680 @@ +package fakes_test + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/fakes" + + h "github.com/buildpacks/imgutil/testhelpers" +) + +const digestDelim = "@" + +func TestFakeIndex(t *testing.T) { + spec.Run(t, "IndexTest", fakeIndex, spec.Parallel(), spec.Report(report.Terminal{})) +} + +func fakeIndex(t *testing.T, when spec.G, it spec.S) { + var ( + fakeDigest name.Digest + err error + ) + it.Before(func() { + fakeDigest, err = name.NewDigest("busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + }) + when("#NewIndex", func() { + it("implements imgutil.ImageIndex", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + var _ imgutil.ImageIndex = idx + }) + when("#NewIndex options", func() { + when("#OS", func() { + it("should return expected os", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + config, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotEq(t, config, nil) + + h.AssertEq(t, os, config.OS) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + os, err := idx.OS(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, os, "") + }) + }) + when("#Architecture", func() { + it("should return expected architecture", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample-image" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + config, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotEq(t, config, nil) + + h.AssertEq(t, arch, config.Architecture) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + arch, err := idx.Architecture(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, arch, "") + }) + }) + when("#Variant", func() { + it("should return expected variant", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + variant, err := idx.Variant(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + config, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotEq(t, config, nil) + + h.AssertEq(t, variant, config.Variant) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + variant, err := idx.Variant(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, variant, "") + }) + }) + when("#OSVersion", func() { + it("should return expected os version", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + osVersion, err := idx.OSVersion(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + config, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotEq(t, config, nil) + + h.AssertEq(t, osVersion, config.OSVersion) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + osVersion, err := idx.OSVersion(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, osVersion, "") + }) + }) + when("#Features", func() { + it("should return expected features", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + features, err := idx.Features(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + config, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotEq(t, config, nil) + + platform := config.Platform() + if platform == nil { + platform = &v1.Platform{} + } + + h.AssertEq(t, features, platform.Features) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + features, err := idx.Features(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, features, []string(nil)) + }) + }) + when("#OSFeatures", func() { + it("should return expected os features", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + config, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotEq(t, config, nil) + + h.AssertEq(t, osFeatures, config.OSFeatures) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + osFeatures, err := idx.OSFeatures(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, osFeatures, []string(nil)) + }) + }) + when("#Annotations", func() { + it("should return expected annotations for oci", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + mfest, err := img.Manifest() + h.AssertNil(t, err) + if mfest == nil { + mfest = &v1.Manifest{} + } + + h.AssertEq(t, annotations, mfest.Annotations) + } + }) + it("should not return annotations for docker", func() { + idx, err := fakes.NewIndex(types.DockerManifestList, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertNil(t, err) + h.AssertEq(t, annotations, map[string]string(nil)) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + annos, err := idx.Annotations(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, annos, map[string]string(nil)) + }) + }) + when("#URLs", func() { + it("should return expected urls", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + urls, err := idx.URLs(digest) + h.AssertNil(t, err) + + img, err := idx.Image(mfest.Digest) + h.AssertNil(t, err) + + mfest, err := img.Manifest() + h.AssertNil(t, err) + + if mfest == nil { + mfest = &v1.Manifest{} + } + + h.AssertEq(t, urls, mfest.Config.URLs) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + urls, err := idx.URLs(fakeDigest) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, urls, []string(nil)) + }) + }) + when("#SetOS", func() { + it("should annotate the image os", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := "some-os" + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + err = idx.SetOS(digest, annotated) + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, annotated) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetOS(fakeDigest, "") + h.AssertNotEq(t, err, nil) + }) + }) + when("#SetArchitecture", func() { + it("should annotate the image architecture", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := "some-arch" + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + err = idx.SetArchitecture(digest, annotated) + h.AssertNil(t, err) + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, annotated) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetArchitecture(fakeDigest, "") + h.AssertNotEq(t, err, nil) + }) + }) + when("#SetVariant", func() { + it("should annotate the image variant", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := "some-variant" + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + err = idx.SetVariant(digest, annotated) + h.AssertNil(t, err) + + variant, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, variant, annotated) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetVariant(fakeDigest, "") + h.AssertNotEq(t, err, nil) + }) + }) + when("#SetOSVersion", func() { + it("should annotate the image os version", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := "some-os-version" + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + err = idx.SetOSVersion(digest, annotated) + h.AssertNil(t, err) + + osVersion, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, osVersion, annotated) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetOSVersion(fakeDigest, "") + h.AssertNotEq(t, err, nil) + }) + }) + when("#SetFeatures", func() { + it("should annotate the features", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := []string{"some-feature"} + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + err = idx.SetFeatures(digest, annotated) + h.AssertNil(t, err) + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, annotated) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetFeatures(fakeDigest, []string{""}) + h.AssertNotEq(t, err, nil) + }) + }) + when("#SetOSFeatures", func() { + it("should annotate the os features", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := []string{"some-os-feature"} + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + err = idx.SetOSFeatures(digest, annotated) + h.AssertNil(t, err) + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, annotated) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetOSFeatures(fakeDigest, []string{""}) + h.AssertNotEq(t, err, nil) + }) + }) + when("#SetAnnotations", func() { + it("should annotate the annotations", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := map[string]string{"some-key": "some-value"} + digest, err := name.NewDigest("cnbs/sample" + digestDelim + idxMfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = idx.SetAnnotations(digest, annotated) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertNil(t, err) + h.AssertEq(t, annotations, annotated) + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetAnnotations(fakeDigest, map[string]string{"some-key": "some-value"}) + h.AssertNotEq(t, err, nil) + }) + }) + when("#SetURLs", func() { + it("should annotate the urls", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + idxMfest, err := idx.IndexManifest() + h.AssertNil(t, err) + + annotated := []string{"some-urls"} + for _, mfest := range idxMfest.Manifests { + digest, err := name.NewDigest("cnbs/sample" + digestDelim + mfest.Digest.String()) + h.AssertNil(t, err) + + err = idx.SetURLs(digest, annotated) + h.AssertNil(t, err) + + urls, err := idx.URLs(digest) + h.AssertNil(t, err) + h.AssertEq(t, urls, annotated) + } + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.SetURLs(fakeDigest, []string{""}) + h.AssertNotEq(t, err, nil) + }) + }) + when("#Add", func() { + it("should add an image", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + digest, err := name.NewDigest("cnbs/sample-image" + digestDelim + "sha256:6d5a11994be8ca5e4cfaf4d370219f6eb6ef8fb41d57f9ed1568a93ffd5471ef") + h.AssertNil(t, err) + err = idx.Add(digest) + h.AssertNil(t, err) + + _, err = idx.OS(digest) + h.AssertNil(t, err) + }) + it("should add from Index", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + digest, err := name.NewDigest("cnbs/sample-image" + digestDelim + "sha256:6d5a11994be8ca5e4cfaf4d370219f6eb6ef8fb41d57f9ed1568a93ffd5471ef") + h.AssertNil(t, err) + err = idx.Add( + digest, + imgutil.WithOS("some-os"), + imgutil.WithArchitecture("some-arch"), + imgutil.WithVariant("some-variant"), + imgutil.WithOSVersion("some-version"), + imgutil.WithFeatures([]string{"some-features"}), + imgutil.WithOSFeatures([]string{"some-osFeatures"}), + imgutil.WithAnnotations(map[string]string{"some-key": "some-value"}), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "some-arch") + + variant, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, variant, "some-variant") + + osVersion, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, osVersion, "some-version") + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, []string{"some-features"}) + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-osFeatures"}) + + annos, err := idx.Annotations(digest) + h.AssertNil(t, err) + h.AssertEq(t, annos, map[string]string{"some-key": "some-value"}) + }) + }) + when("#Save", func() { + it("should save image", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + }) + it("should return an error", func() {}) + }) + when("#Push", func() { + it("should push index to registry", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.Push() + h.AssertNil(t, err) + }) + it("should return an error", func() {}) + }) + when("#Inspect", func() { + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mfest, err := idx.Inspect() + h.AssertNil(t, err) + h.AssertNotEq(t, mfest, "") + }) + }) + when("#Delete", func() { + it("should delete index from local storage", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should return an error", func() { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + err = idx.Delete() + h.AssertNil(t, err) + + err = idx.Delete() + h.AssertNotEq(t, err, nil) + }) + }) + }) + }) +} diff --git a/fakes/layer.go b/fakes/layer.go new file mode 100644 index 00000000..a72518b4 --- /dev/null +++ b/fakes/layer.go @@ -0,0 +1,59 @@ +package fakes + +import ( + "archive/tar" + "bytes" + "crypto" + "encoding/hex" + "fmt" + "io" + "math/rand" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Layer returns a layer with pseudo-randomly generated content. +func Layer(byteSize int64, mt types.MediaType, options ...Option) (v1.Layer, error) { + o := getOptions(options) + rng := rand.New(o.source) //nolint:gosec + + fileName := fmt.Sprintf("random_file_%d.txt", rng.Int()) + + // Hash the contents as we write it out to the buffer. + var b bytes.Buffer + hasher := crypto.SHA256.New() + mw := io.MultiWriter(&b, hasher) + + // Write a single file with a random name and random contents. + tw := tar.NewWriter(mw) + if err := tw.WriteHeader(&tar.Header{ + Name: fileName, + Size: byteSize, + Typeflag: tar.TypeReg, + }); err != nil { + return nil, err + } + if _, err := io.CopyN(tw, rng, byteSize); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + + h := v1.Hash{ + Algorithm: "sha256", + Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), + } + + if o.withHash != (v1.Hash{}) { + h = o.withHash + } + + return partial.UncompressedToLayer(&uncompressedLayer{ + diffID: h, + mediaType: mt, + content: b.Bytes(), + }) +} diff --git a/fakes/options.go b/fakes/options.go new file mode 100644 index 00000000..c519c55f --- /dev/null +++ b/fakes/options.go @@ -0,0 +1,84 @@ +package fakes + +import ( + "errors" + "math/rand" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type IndexAddOption func(*IndexAddOptions) error +type IndexPushOption func(*IndexPushOptions) error + +type IndexAddOptions struct { + format types.MediaType +} +type IndexPushOptions struct{} + +func WithFormat(format types.MediaType) IndexAddOption { + return func(o *IndexAddOptions) error { + if !format.IsImage() { + return errors.New("unsupported format") + } + o.format = format + return nil + } +} + +// Option is an optional parameter to the random functions +type Option func(opts *options) + +type options struct { + source rand.Source + withIndex bool + withHash v1.Hash + + // TODO opens the door to add this in the future + // algorithm digest.Algorithm +} + +func getOptions(opts []Option) *options { + // get a random seed + + // TODO in go 1.20 this is fine (it will be random) + seed := rand.Int63() //nolint:gosec + /* + // in prior go versions this needs to come from crypto/rand + var b [8]byte + _, err := crypto_rand.Read(b[:]) + if err != nil { + panic("cryptographically secure random number generator is not working") + } + seed := int64(binary.LittleEndian.Int64(b[:])) + */ + + // defaults + o := &options{ + source: rand.NewSource(seed), + } + + for _, opt := range opts { + opt(o) + } + return o +} + +// WithSource sets the random number generator source +func WithSource(source rand.Source) Option { + return func(opts *options) { + opts.source = source + } +} + +func WithIndex(withIndex bool) Option { + return func(opts *options) { + opts.withIndex = withIndex + } +} + +func WithHash(hash v1.Hash) Option { + return func(opts *options) { + opts.withHash = hash + } +} diff --git a/image.go b/image.go index 87bd82c3..1e3b2fb3 100644 --- a/image.go +++ b/image.go @@ -8,12 +8,44 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/types" ) +type EditableImage interface { + // Getters + + OS() (string, error) + Architecture() (string, error) + Variant() (string, error) + OSVersion() (string, error) + Features() ([]string, error) + OSFeatures() ([]string, error) + URLs() ([]string, error) + Annotations() (map[string]string, error) + + // Setters + + SetOS(string) error + SetArchitecture(string) error + SetVariant(string) error + SetOSVersion(string) error + SetFeatures([]string) error + SetOSFeatures([]string) error + SetURLs([]string) error + SetAnnotations(map[string]string) error + + // misc + + MediaType() (types.MediaType, error) + Digest() (v1.Hash, error) + // ManifestSize returns the size of the manifest. If a manifest doesn't exist, it returns 0. + ManifestSize() (int64, error) +} + type Image interface { + EditableImage // getters - Architecture() (string, error) CreatedAt() (time.Time, error) Entrypoint() ([]string, error) Env(key string) (string, error) @@ -29,17 +61,12 @@ type Image interface { Kind() string Label(string) (string, error) Labels() (map[string]string, error) - // ManifestSize returns the size of the manifest. If a manifest doesn't exist, it returns 0. - ManifestSize() (int64, error) Name() string - OS() (string, error) - OSVersion() (string, error) // TopLayer returns the diff id for the top layer TopLayer() (string, error) UnderlyingImage() v1.Image // Valid returns true if the image is well-formed (e.g. all manifest layers exist on the registry). Valid() bool - Variant() (string, error) WorkingDir() (string, error) // setters @@ -47,15 +74,11 @@ type Image interface { // AnnotateRefName set a value for the `org.opencontainers.image.ref.name` annotation AnnotateRefName(refName string) error Rename(name string) - SetArchitecture(string) error SetCmd(...string) error SetEntrypoint(...string) error SetEnv(string, string) error SetHistory([]v1.History) error SetLabel(string, string) error - SetOS(string) error - SetOSVersion(string) error - SetVariant(string) error SetWorkingDir(string) error // modifiers @@ -76,6 +99,13 @@ type Image interface { SaveFile() (string, error) } +const ( + LOCAL = "local" + LAYOUT = "layout" + REMOTE = "remote" + LOCALLAYOUT = "locallayout" +) + type Identifier fmt.Stringer // Platform represents the target arch/os/os_version for an image construction and querying. diff --git a/index.go b/index.go index 691ad20a..a97ab813 100644 --- a/index.go +++ b/index.go @@ -1,47 +1,1582 @@ package imgutil import ( + "context" + "encoding/json" + "errors" "fmt" - "strings" + "os" + "path/filepath" + "runtime" + "sync" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/types" + "golang.org/x/sync/errgroup" ) +// An Interface with list of Methods required for creation and manipulation of v1.IndexManifest type ImageIndex interface { // getters - Name() string + OS(digest name.Digest) (os string, err error) + Architecture(digest name.Digest) (arch string, err error) + Variant(digest name.Digest) (osVariant string, err error) + OSVersion(digest name.Digest) (osVersion string, err error) + Features(digest name.Digest) (features []string, err error) + OSFeatures(digest name.Digest) (osFeatures []string, err error) + Annotations(digest name.Digest) (annotations map[string]string, err error) + URLs(digest name.Digest) (urls []string, err error) - // modifiers - Add(repoName string) error - Remove(repoName string) error - Save(additionalNames ...string) error + // setters + + SetOS(digest name.Digest, os string) error + SetArchitecture(digest name.Digest, arch string) error + SetVariant(digest name.Digest, osVariant string) error + SetOSVersion(digest name.Digest, osVersion string) error + SetFeatures(digest name.Digest, features []string) error + SetOSFeatures(digest name.Digest, osFeatures []string) error + SetAnnotations(digest name.Digest, annotations map[string]string) error + SetURLs(digest name.Digest, urls []string) error + + // misc + + Add(ref name.Reference, ops ...IndexAddOption) error + Save() error + Push(ops ...IndexPushOption) error + Inspect() (string, error) + Remove(ref name.Reference) error + Delete() error +} + +var ( + ErrOSUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image os is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrArchUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image architecture is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrVariantUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image variant is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrOSVersionUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image os-version is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrFeaturesUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image features is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrOSFeaturesUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image os-features is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrURLsUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image urls is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrAnnotationsUndefined = func(format types.MediaType, digest string) error { + return fmt.Errorf("Image annotations is undefined for %s ImageIndex (digest: %s)", indexMediaType(format), digest) + } + ErrNoImageOrIndexFoundWithGivenDigest = func(digest string) error { + return fmt.Errorf(`no image or image index found for digest "%s"`, digest) + } + ErrConfigFilePlatformUndefined = errors.New("unable to determine image platform: ConfigFile's platform is nil") + ErrManifestUndefined = errors.New("encountered unexpected error while parsing image: manifest or index manifest is nil") + ErrPlatformUndefined = errors.New("unable to determine image platform: platform is nil") + ErrInvalidPlatform = errors.New("unable to determine image platform: platform's 'OS' or 'Architecture' field is nil") + ErrConfigFileUndefined = errors.New("unable to access image configuration: ConfigFile is nil") + ErrIndexNeedToBeSaved = errors.New(`unable to perform action: ImageIndex requires local storage before proceeding. + Please use '#Save()' to save the image index locally before attempting this operation`) + ErrUnknownMediaType = func(format types.MediaType) error { + return fmt.Errorf("unsupported media type encountered in image: '%s'", format) + } + ErrNoImageFoundWithGivenPlatform = errors.New("no image found for specified platform") +) + +var _ ImageIndex = (*ManifestHandler)(nil) + +// A Handler implementing ImageIndex. +// Creates and Manipulate IndexManifest. +type ManifestHandler struct { + v1.ImageIndex + Annotate Annotate + Options IndexOptions + RemovedManifests []v1.Hash + Images map[v1.Hash]v1.Descriptor +} + +func (h *ManifestHandler) getHash(digest name.Digest) (hash v1.Hash, err error) { + if hash, err = v1.NewHash(digest.Identifier()); err != nil { + return hash, err + } + + // if any image is removed with given hash return an error + for _, h := range h.RemovedManifests { + if h == hash { + return hash, ErrNoImageOrIndexFoundWithGivenDigest(h.String()) + } + } + + return hash, nil +} + +// Returns `OS` of an existing Image. +func (h *ManifestHandler) OS(digest name.Digest) (os string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return os, err + } + + // if image is manipulated before return last manipulated value + if os, err = h.Annotate.OS(hash); err == nil { + return os, nil + } + + getOS := func(desc v1.Descriptor) (os string, err error) { + if desc.Platform == nil { + return os, ErrPlatformUndefined + } + + if desc.Platform.OS == "" { + return os, ErrOSUndefined(desc.MediaType, hash.String()) + } + + return desc.Platform.OS, nil + } + + // return the OS of the added image(using ImageIndex#Add) if found + if desc, ok := h.Images[hash]; ok { + return getOS(desc) + } + + // check for the digest in the IndexManifest and return `OS` if found + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return os, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return getOS(desc) + } + } + + // when no image found with the given digest return an error + return os, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Annotates existing Image by updating `OS` field in IndexManifest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) SetOS(digest name.Digest, os string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + // if any nested imageIndex found with given digest save underlying image instead of index with the given OS + if mfest, err := h.getIndexManifest(digest); err == nil { + // keep track of changes until ImageIndex#Save is called + h.Annotate.SetOS(hash, os) + h.Annotate.SetFormat(hash, mfest.MediaType) + + return nil + } + + // set the `OS` of an Image from base ImageIndex if found + if img, err := h.Image(hash); err == nil { + return h.setImageOS(img, hash, os) + } + + // set the `OS` of an Image added to ImageIndex if found + if desc, ok := h.Images[hash]; ok { + // keep track of changes until ImageIndex#Save is called + h.Annotate.SetOS(hash, os) + h.Annotate.SetFormat(hash, desc.MediaType) + + return nil + } + + // return an error if no Image found given digest + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Add requested OS to `Annotate` +func (h *ManifestHandler) setImageOS(img v1.Image, hash v1.Hash, os string) error { + mfest, err := getManifest(img) + if err != nil { + return err + } + + h.Annotate.SetOS(hash, os) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil +} + +// Return the Architecture of an Image/Index based on given Digest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) Architecture(digest name.Digest) (arch string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return arch, err + } + + if arch, err = h.Annotate.Architecture(hash); err == nil { + return arch, nil + } + + getArch := func(desc v1.Descriptor) (arch string, err error) { + if desc.Platform == nil { + return arch, ErrPlatformUndefined + } + + if desc.Platform.Architecture == "" { + return arch, ErrArchUndefined(desc.MediaType, hash.String()) + } + + return desc.Platform.Architecture, nil + } + + if desc, ok := h.Images[hash]; ok { + return getArch(desc) + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return arch, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return getArch(desc) + } + } + + return arch, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Annotates the `Architecture` of an Image. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) SetArchitecture(digest name.Digest, arch string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + if mfest, err := h.getIndexManifest(digest); err == nil { + h.Annotate.SetArchitecture(hash, arch) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil + } + + if img, err := h.Image(hash); err == nil { + return h.setImageArch(img, hash, arch) + } + + if desc, ok := h.Images[hash]; ok { + h.Annotate.SetArchitecture(hash, arch) + h.Annotate.SetFormat(hash, desc.MediaType) + return nil + } + + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Add request ARCH to `Annotate` +func (h *ManifestHandler) setImageArch(img v1.Image, hash v1.Hash, arch string) error { + mfest, err := getManifest(img) + if err != nil { + return err + } + + h.Annotate.SetArchitecture(hash, arch) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil +} + +// Return the `Variant` of an Image. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) Variant(digest name.Digest) (osVariant string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return osVariant, err + } + + if osVariant, err = h.Annotate.Variant(hash); err == nil { + return osVariant, err + } + + getVariant := func(desc v1.Descriptor) (osVariant string, err error) { + if desc.Platform == nil { + return osVariant, ErrPlatformUndefined + } + + if desc.Platform.Variant == "" { + return osVariant, ErrVariantUndefined(desc.MediaType, hash.String()) + } + + return desc.Platform.Variant, nil + } + + if desc, ok := h.Images[hash]; ok { + return getVariant(desc) + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return osVariant, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return getVariant(desc) + } + } + + return osVariant, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Annotates the `Variant` of an Image with given Digest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) SetVariant(digest name.Digest, osVariant string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + if mfest, err := h.getIndexManifest(digest); err == nil { + h.Annotate.SetVariant(hash, osVariant) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil + } + + if img, err := h.Image(hash); err == nil { + return h.setImageVariant(img, hash, osVariant) + } + + if desc, ok := h.Images[hash]; ok { + h.Annotate.SetVariant(hash, osVariant) + h.Annotate.SetFormat(hash, desc.MediaType) + return nil + } + + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Add requested OSVariant to `Annotate`. +func (h *ManifestHandler) setImageVariant(img v1.Image, hash v1.Hash, osVariant string) error { + mfest, err := getManifest(img) + if err != nil { + return err + } + + h.Annotate.SetVariant(hash, osVariant) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil +} + +// Returns the `OSVersion` of an Image with given Digest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) OSVersion(digest name.Digest) (osVersion string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return osVersion, err + } + + if osVersion, err = h.Annotate.OSVersion(hash); err == nil { + return osVersion, nil + } + + getOSVersion := func(desc v1.Descriptor) (osVersion string, err error) { + if desc.Platform == nil { + return osVersion, ErrPlatformUndefined + } + + if desc.Platform.OSVersion == "" { + return osVersion, ErrOSVersionUndefined(desc.MediaType, hash.String()) + } + + return desc.Platform.OSVersion, nil + } + + if desc, ok := h.Images[hash]; ok { + return getOSVersion(desc) + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return osVersion, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return getOSVersion(desc) + } + } + + return osVersion, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Annotates the `OSVersion` of an Image with given Digest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) SetOSVersion(digest name.Digest, osVersion string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + if mfest, err := h.getIndexManifest(digest); err == nil { + h.Annotate.SetOSVersion(hash, osVersion) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil + } + + if img, err := h.Image(hash); err == nil { + return h.setImageOSVersion(img, hash, osVersion) + } + + if desc, ok := h.Images[hash]; ok { + h.Annotate.SetOSVersion(hash, osVersion) + h.Annotate.SetFormat(hash, desc.MediaType) + return nil + } + + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Add requested OSVersion to `Annotate` +func (h *ManifestHandler) setImageOSVersion(img v1.Image, hash v1.Hash, osVersion string) error { + mfest, err := getManifest(img) + if err != nil { + return err + } + + h.Annotate.SetOSVersion(hash, osVersion) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil +} + +// Returns the `Features` of an Image with given Digest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) Features(digest name.Digest) (features []string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return features, err + } + + if features, err = h.Annotate.Features(hash); err == nil { + return features, nil + } + + if features, err = h.indexFeatures(digest); err == nil { + return features, nil + } + + getFeatures := func(desc v1.Descriptor) (features []string, err error) { + if desc.Platform == nil { + return features, ErrPlatformUndefined + } + + if len(desc.Platform.Features) == 0 { + return features, ErrFeaturesUndefined(desc.MediaType, hash.String()) + } + + var featuresSet = NewStringSet() + for _, f := range desc.Platform.Features { + featuresSet.Add(f) + } + + return featuresSet.StringSlice(), nil + } + + if desc, ok := h.Images[hash]; ok { + return getFeatures(desc) + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return features, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return getFeatures(desc) + } + } + + return features, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Returns Features from IndexManifest. +func (h *ManifestHandler) indexFeatures(digest name.Digest) (features []string, err error) { + mfest, err := h.getIndexManifest(digest) + if err != nil { + return + } + + if mfest.Subject == nil { + mfest.Subject = &v1.Descriptor{} + } + + if mfest.Subject.Platform == nil { + mfest.Subject.Platform = &v1.Platform{} + } + + if len(mfest.Subject.Platform.Features) == 0 { + return features, ErrFeaturesUndefined(mfest.MediaType, digest.Identifier()) + } + + return mfest.Subject.Platform.Features, nil +} + +// Annotates the `Features` of an Image with given Digest by appending to existsing Features if any. +// +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) SetFeatures(digest name.Digest, features []string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + if mfest, err := h.getIndexManifest(digest); err == nil { + h.Annotate.SetFeatures(hash, features) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil + } + + if img, err := h.Image(hash); err == nil { + return h.setImageFeatures(img, hash, features) + } + + if desc, ok := h.Images[hash]; ok { + h.Annotate.SetFeatures(hash, features) + h.Annotate.SetFormat(hash, desc.MediaType) + return nil + } + + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (h *ManifestHandler) setImageFeatures(img v1.Image, hash v1.Hash, features []string) error { + mfest, err := getManifest(img) + if err != nil { + return err + } + + h.Annotate.SetFeatures(hash, features) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil +} + +// Returns the `OSFeatures` of an Image with given Digest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) OSFeatures(digest name.Digest) (osFeatures []string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return osFeatures, err + } + + if osFeatures, err = h.Annotate.OSFeatures(hash); err == nil { + return osFeatures, nil + } + + osFeatures, err = h.indexOSFeatures(digest) + if err == nil { + return osFeatures, nil + } + + getOSFeatures := func(desc v1.Descriptor) (osFeatures []string, err error) { + if desc.Platform == nil { + return osFeatures, ErrPlatformUndefined + } + + if len(desc.Platform.OSFeatures) == 0 { + return osFeatures, ErrOSFeaturesUndefined(desc.MediaType, digest.Identifier()) + } + + var osFeaturesSet = NewStringSet() + for _, s := range desc.Platform.OSFeatures { + osFeaturesSet.Add(s) + } + + return osFeaturesSet.StringSlice(), nil + } + + if desc, ok := h.Images[hash]; ok { + return getOSFeatures(desc) + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return osFeatures, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return getOSFeatures(desc) + } + } + + return osFeatures, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Returns OSFeatures from IndexManifest. +func (h *ManifestHandler) indexOSFeatures(digest name.Digest) (osFeatures []string, err error) { + mfest, err := h.getIndexManifest(digest) + if err != nil { + return osFeatures, err + } + + if mfest.Subject == nil { + mfest.Subject = &v1.Descriptor{} + } + + if mfest.Subject.Platform == nil { + mfest.Subject.Platform = &v1.Platform{} + } + + if len(mfest.Subject.Platform.OSFeatures) == 0 { + return osFeatures, ErrOSFeaturesUndefined(mfest.MediaType, digest.Identifier()) + } + + return mfest.Subject.Platform.OSFeatures, nil +} + +// Annotates the `OSFeatures` of an Image with given Digest by appending to existsing OSFeatures if any. +// +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) SetOSFeatures(digest name.Digest, osFeatures []string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + if mfest, err := h.getIndexManifest(digest); err == nil { + h.Annotate.SetOSFeatures(hash, osFeatures) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil + } + + if img, err := h.Image(hash); err == nil { + return h.setImageOSFeatures(img, hash, osFeatures) + } + + if desc, ok := h.Images[hash]; ok { + h.Annotate.SetOSFeatures(hash, osFeatures) + h.Annotate.SetFormat(hash, desc.MediaType) + return nil + } + + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (h *ManifestHandler) setImageOSFeatures(img v1.Image, hash v1.Hash, osFeatures []string) error { + mfest, err := getManifest(img) + if err != nil { + return err + } + + h.Annotate.SetOSFeatures(hash, osFeatures) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil +} + +// Return the `Annotations` of an Image with given Digest. +// Returns an error if no Image/Index found with given Digest. +// +// For Docker Images and Indexes it returns an error. +func (h *ManifestHandler) Annotations(digest name.Digest) (annotations map[string]string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return annotations, err + } + + getAnnotations := func(annos map[string]string, format types.MediaType) (map[string]string, error) { + switch format { + case types.DockerManifestSchema2, + types.DockerManifestSchema1, + types.DockerManifestSchema1Signed, + types.DockerManifestList: + // Docker Manifest doesn't support annotations + return nil, ErrAnnotationsUndefined(format, digest.Identifier()) + case types.OCIManifestSchema1, + types.OCIImageIndex: + if len(annos) == 0 { + return nil, ErrAnnotationsUndefined(format, digest.Identifier()) + } + + return annos, nil + default: + return annos, ErrUnknownMediaType(format) + } + } + + if annotations, err = h.Annotate.Annotations(hash); err == nil { + format, err := h.Annotate.Format(hash) + if err != nil { + return annotations, err + } + + return getAnnotations(annotations, format) + } + + annotations, format, err := h.indexAnnotations(digest) + if err == nil || errors.Is(err, ErrAnnotationsUndefined(format, digest.Identifier())) { + return annotations, err + } + + if desc, ok := h.Images[hash]; ok { + return getAnnotations(desc.Annotations, desc.MediaType) + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return annotations, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return getAnnotations(desc.Annotations, desc.MediaType) + } + } + + return annotations, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +func (h *ManifestHandler) indexAnnotations(digest name.Digest) (annotations map[string]string, format types.MediaType, err error) { + mfest, err := h.getIndexManifest(digest) + if err != nil { + return + } + + if len(mfest.Annotations) == 0 { + return annotations, types.DockerConfigJSON, ErrAnnotationsUndefined(mfest.MediaType, digest.Identifier()) + } + + if mfest.MediaType == types.DockerManifestList { + return nil, types.DockerManifestList, ErrAnnotationsUndefined(mfest.MediaType, digest.Identifier()) + } + + return mfest.Annotations, types.OCIImageIndex, nil +} + +// Annotates the `Annotations` of an Image with given Digest by appending to existsing Annotations if any. +// +// Returns an error if no Image/Index found with given Digest. +// +// For Docker Images and Indexes it ignores updating Annotations. +func (h *ManifestHandler) SetAnnotations(digest name.Digest, annotations map[string]string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + annos := mfest.Annotations + if len(annos) == 0 { + annos = make(map[string]string) + } + + for k, v := range annotations { + annos[k] = v + } + + h.Annotate.SetAnnotations(hash, annos) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil + } + } + + if desc, ok := h.Images[hash]; ok { + annos := make(map[string]string, 0) + if len(desc.Annotations) != 0 { + annos = desc.Annotations + } + + for k, v := range annotations { + annos[k] = v + } + + h.Annotate.SetAnnotations(hash, annos) + h.Annotate.SetFormat(hash, desc.MediaType) + return nil + } + + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Returns the `URLs` of an Image with given Digest. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) URLs(digest name.Digest) (urls []string, err error) { + hash, err := h.getHash(digest) + if err != nil { + return urls, err + } + + if urls, err = h.Annotate.URLs(hash); err == nil { + var urlSet = NewStringSet() + for _, s := range urls { + urlSet.Add(s) + } + return urlSet.StringSlice(), nil + } + + if urls, err = h.getIndexURLs(hash); err == nil { + return urls, nil + } + + urls, format, err := h.getImageURLs(hash) + if err == nil { + return urls, nil + } + + if err == ErrURLsUndefined(format, digest.Identifier()) { + return urls, ErrURLsUndefined(format, digest.Identifier()) + } + + return urls, ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Annotates the `URLs` of an Image with given Digest by appending to existsing URLs if any. +// Returns an error if no Image/Index found with given Digest. +func (h *ManifestHandler) SetURLs(digest name.Digest, urls []string) error { + hash, err := h.getHash(digest) + if err != nil { + return err + } + + if mfest, err := h.getIndexManifest(digest); err == nil { + h.Annotate.SetURLs(hash, urls) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil + } + + if img, err := h.Image(hash); err == nil { + return h.setImageURLs(img, hash, urls) + } + + if desc, ok := h.Images[hash]; ok { + h.Annotate.SetURLs(hash, urls) + h.Annotate.SetFormat(hash, desc.MediaType) + return nil + } + + return ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()) +} + +// Adds the requested URLs to `Annotate`. +func (h *ManifestHandler) setImageURLs(img v1.Image, hash v1.Hash, urls []string) error { + mfest, err := getManifest(img) + if err != nil { + return err + } + + h.Annotate.SetURLs(hash, urls) + h.Annotate.SetFormat(hash, mfest.MediaType) + return nil } -func (t MediaTypes) IndexManifestType() types.MediaType { - switch t { - case OCITypes: - return types.OCIImageIndex - case DockerTypes: - return types.DockerManifestList +// Add the ImageIndex from the registry with the given Reference. +// +// If referencing an ImageIndex, will add Platform Specific Image from the Index. +// Use IndexAddOptions to alter behaviour for ImageIndex Reference. +func (h *ManifestHandler) Add(ref name.Reference, ops ...IndexAddOption) error { + var addOps = &AddOptions{} + for _, op := range ops { + op(addOps) + } + + layoutPath := filepath.Join(h.Options.XdgPath, h.Options.Reponame) + path, pathErr := layout.FromPath(layoutPath) + if addOps.Local { + if pathErr != nil { + return pathErr + } + img := addOps.Image + var ( + os, _ = img.OS() + arch, _ = img.Architecture() + variant, _ = img.Variant() + osVersion, _ = img.OSVersion() + features, _ = img.Features() + osFeatures, _ = img.OSFeatures() + urls, _ = img.URLs() + annos, _ = img.Annotations() + size, _ = img.ManifestSize() + mediaType, err = img.MediaType() + digest, _ = img.Digest() + ) + if err != nil { + return err + } + + desc := v1.Descriptor{ + MediaType: mediaType, + Size: size, + Digest: digest, + URLs: urls, + Annotations: annos, + Platform: &v1.Platform{ + OS: os, + Architecture: arch, + Variant: variant, + OSVersion: osVersion, + Features: features, + OSFeatures: osFeatures, + }, + } + + return path.AppendDescriptor(desc) + } + + // Fetch Descriptor of the given reference. + // + // This call is returns a v1.Descriptor with `Size`, `MediaType`, `Digest` fields only!! + // This is a light weight call used for checking MediaType of given Reference + desc, err := remote.Head( + ref, + remote.WithAuthFromKeychain(h.Options.KeyChain), + remote.WithTransport(GetTransport(h.Options.Insecure())), + ) + if err != nil { + return err + } + + if desc == nil { + return ErrManifestUndefined + } + + switch { + case desc.MediaType.IsImage(): + // Get the Full Image from remote if the given Reference refers an Image + img, err := remote.Image( + ref, + remote.WithAuthFromKeychain(h.Options.KeyChain), + remote.WithTransport(GetTransport(h.Options.Insecure())), + ) + if err != nil { + return err + } + + mfest, err := getManifest(img) + if err != nil { + return err + } + + imgConfig, err := getConfigFile(img) + if err != nil { + return err + } + + platform := v1.Platform{} + if err := updatePlatform(imgConfig, &platform); err != nil { + return err + } + + // update the v1.Descriptor with expected MediaType, Size, and Digest + // since mfest.Subject can be nil using mfest.Config is safer + config := mfest.Config + config.Digest = desc.Digest + config.MediaType = desc.MediaType + config.Size = desc.Size + config.Platform = &platform + config.Annotations = mfest.Annotations + + // keep tract of newly added Image + h.Images[desc.Digest] = config + if config.MediaType == types.OCIManifestSchema1 && len(addOps.Annotations) != 0 { + if len(config.Annotations) == 0 { + config.Annotations = make(map[string]string) + } + + for k, v := range addOps.Annotations { + config.Annotations[k] = v + } + } + + if pathErr != nil { + path, err = layout.Write(layoutPath, h.ImageIndex) + if err != nil { + return err + } + } + + // Append Image to V1.ImageIndex with the Annotations if any + return path.AppendDescriptor(config) + case desc.MediaType.IsIndex(): + switch { + case addOps.All: + idx, err := remote.Index( + ref, + remote.WithAuthFromKeychain(h.Options.KeyChain), + remote.WithTransport(GetTransport(h.Options.Insecure())), + ) + if err != nil { + return err + } + + var iMap sync.Map + errs := SaveError{} + // Add all the Images from Nested ImageIndexes + if err = h.addAllImages(idx, addOps.Annotations, &iMap); err != nil { + return err + } + + if err != nil { + // if the ImageIndex is not saved till now for some reason Save the ImageIndex locally to append Images + if err = h.Save(); err != nil { + return err + } + } + + iMap.Range(func(key, value any) bool { + desc, ok := value.(v1.Descriptor) + if !ok { + return false + } + + digest, ok := key.(v1.Hash) + if !ok { + return false + } + + h.Images[digest] = desc + + // Append All the Images within the nested ImageIndexes + if err = path.AppendDescriptor(desc); err != nil { + errs.Errors = append(errs.Errors, SaveDiagnostic{ + Cause: err, + }) + } + return true + }) + + if len(errs.Errors) != 0 { + return errs + } + + return nil + case addOps.OS != "", + addOps.Arch != "", + addOps.Variant != "", + addOps.OSVersion != "", + len(addOps.Features) != 0, + len(addOps.OSFeatures) != 0: + + platformSpecificDesc := &v1.Platform{} + if addOps.OS != "" { + platformSpecificDesc.OS = addOps.OS + } + + if addOps.Arch != "" { + platformSpecificDesc.Architecture = addOps.Arch + } + + if addOps.Variant != "" { + platformSpecificDesc.Variant = addOps.Variant + } + + if addOps.OSVersion != "" { + platformSpecificDesc.OSVersion = addOps.OSVersion + } + + if len(addOps.Features) != 0 { + platformSpecificDesc.Features = addOps.Features + } + + if len(addOps.OSFeatures) != 0 { + platformSpecificDesc.OSFeatures = addOps.OSFeatures + } + + // Add an Image from the ImageIndex with the given Platform + return h.addPlatformSpecificImages(ref, *platformSpecificDesc, addOps.Annotations) + default: + platform := v1.Platform{ + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + } + + // Add the Image from the ImageIndex with current Device's Platform + return h.addPlatformSpecificImages(ref, platform, addOps.Annotations) + } default: - return "" + // return an error if the Reference is neither an Image not an Index + return ErrUnknownMediaType(desc.MediaType) + } +} + +func (h *ManifestHandler) addAllImages(idx v1.ImageIndex, annotations map[string]string, imageMap *sync.Map) error { + mfest, err := getIndexManifest(idx) + if err != nil { + return err + } + + var errs, _ = errgroup.WithContext(context.Background()) + for _, desc := range mfest.Manifests { + desc := desc + errs.Go(func() error { + return h.addIndexAddendum(annotations, desc, idx, imageMap) + }) + } + + return errs.Wait() +} + +func (h *ManifestHandler) addIndexAddendum(annotations map[string]string, desc v1.Descriptor, idx v1.ImageIndex, iMap *sync.Map) error { + switch { + case desc.MediaType.IsIndex(): + ii, err := idx.ImageIndex(desc.Digest) + if err != nil { + return err + } + + return h.addAllImages(ii, annotations, iMap) + case desc.MediaType.IsImage(): + img, err := idx.Image(desc.Digest) + if err != nil { + return err + } + + mfest, err := getManifest(img) + if err != nil { + return err + } + + imgConfig, err := img.ConfigFile() + if err != nil { + return err + } + + platform := v1.Platform{} + if err = updatePlatform(imgConfig, &platform); err != nil { + return err + } + + config := mfest.Config.DeepCopy() + config.Size = desc.Size + config.MediaType = desc.MediaType + config.Digest = desc.Digest + config.Platform = &platform + config.Annotations = mfest.Annotations + + if len(config.Annotations) == 0 { + config.Annotations = make(map[string]string, 0) + } + + if len(annotations) != 0 && mfest.MediaType == types.OCIManifestSchema1 { + for k, v := range annotations { + config.Annotations[k] = v + } + } + + h.Images[desc.Digest] = *config + iMap.Store(desc.Digest, *config) + + return nil + default: + return ErrUnknownMediaType(desc.MediaType) + } +} + +func (h *ManifestHandler) addPlatformSpecificImages(ref name.Reference, platform v1.Platform, annotations map[string]string) error { + if platform.OS == "" || platform.Architecture == "" { + return ErrInvalidPlatform } + + desc, err := remote.Get( + ref, + remote.WithAuthFromKeychain(h.Options.KeyChain), + remote.WithTransport(GetTransport(true)), + remote.WithPlatform(platform), + ) + if err != nil { + return err + } + + img, err := desc.Image() + if err != nil { + return err + } + + digest, err := img.Digest() + if err != nil { + return err + } + + mfest, err := getManifest(img) + if err != nil { + return err + } + + imgConfig, err := getConfigFile(img) + if err != nil { + return err + } + + platform = v1.Platform{} + if err = updatePlatform(imgConfig, &platform); err != nil { + return err + } + + config := mfest.Config.DeepCopy() + config.MediaType = mfest.MediaType + config.Digest = digest + config.Size = desc.Size + config.Platform = &platform + config.Annotations = mfest.Annotations + + if len(config.Annotations) != 0 { + config.Annotations = make(map[string]string, 0) + } + + if len(annotations) != 0 && config.MediaType == types.OCIManifestSchema1 { + for k, v := range annotations { + config.Annotations[k] = v + } + } + + h.Images[digest] = *config + + layoutPath := filepath.Join(h.Options.XdgPath, h.Options.Reponame) + path, err := layout.FromPath(layoutPath) + if err != nil { + if path, err = layout.Write(layoutPath, h.ImageIndex); err != nil { + return err + } + } + + return path.AppendDescriptor(*config) +} + +// Save IndexManifest locally. +// Use it save manifest locally iff the manifest doesn't exist locally before +func (h *ManifestHandler) save(layoutPath string) (path layout.Path, err error) { + // If the ImageIndex is not saved before Save the ImageIndex + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return path, err + } + + // Initially write an empty IndexManifest with expected MediaType + if mfest.MediaType == types.OCIImageIndex { + if path, err = layout.Write(layoutPath, empty.Index); err != nil { + return path, err + } + } else { + if path, err = layout.Write(layoutPath, NewEmptyDockerIndex()); err != nil { + return path, err + } + } + + // loop over each digest and append Image/ImageIndex + for _, d := range mfest.Manifests { + switch { + case d.MediaType.IsIndex(), d.MediaType.IsImage(): + if err = path.AppendDescriptor(d); err != nil { + return path, err + } + default: + return path, ErrUnknownMediaType(d.MediaType) + } + } + + return path, nil +} + +// Save will locally save the given ImageIndex. +func (h *ManifestHandler) Save() error { + layoutPath := filepath.Join(h.Options.XdgPath, h.Options.Reponame) + path, err := layout.FromPath(layoutPath) + if err != nil { + if path, err = h.save(layoutPath); err != nil { + return err + } + } + + hashes := make([]v1.Hash, 0, len(h.Annotate.Instance)) + for h := range h.Annotate.Instance { + hashes = append(hashes, h) + } + + // Remove all the Annotated Images/ImageIndexes from local ImageIndex to avoid duplicate Images with same Digest + if err = path.RemoveDescriptors(match.Digests(hashes...)); err != nil { + return err + } + + var errs SaveError + for hash, desc := range h.Annotate.Instance { + // If the digest matches an Image added annotate the Image and Save Locally + if imgDesc, ok := h.Images[hash]; ok { + if !imgDesc.MediaType.IsImage() && !imgDesc.MediaType.IsIndex() { + return ErrUnknownMediaType(imgDesc.MediaType) + } + + appendAnnotatedManifests(desc, imgDesc, path, &errs) + continue + } + + // Using IndexManifest annotate required changes + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return err + } + + var imageFound = false + for _, imgDesc := range mfest.Manifests { + if imgDesc.Digest == hash { + imageFound = true + if !imgDesc.MediaType.IsImage() && !imgDesc.MediaType.IsIndex() { + return ErrUnknownMediaType(imgDesc.MediaType) + } + + appendAnnotatedManifests(desc, imgDesc, path, &errs) + break + } + } + + if !imageFound { + return ErrNoImageOrIndexFoundWithGivenDigest(hash.String()) + } + } + + if len(errs.Errors) != 0 { + return errs + } + + var removeHashes = make([]v1.Hash, 0) + for _, hash := range h.RemovedManifests { + if _, ok := h.Images[hash]; !ok { + removeHashes = append(removeHashes, hash) + delete(h.Images, hash) + } + } + + h.Annotate = Annotate{ + Instance: make(map[v1.Hash]v1.Descriptor, 0), + } + h.RemovedManifests = make([]v1.Hash, 0) + return path.RemoveDescriptors(match.Digests(removeHashes...)) +} + +// Publishes ImageIndex to the registry assuming every image it referes exists in registry. +// +// It will only push the IndexManifest to registry. +func (h *ManifestHandler) Push(ops ...IndexPushOption) error { + if len(h.RemovedManifests) != 0 || len(h.Annotate.Instance) != 0 { + return ErrIndexNeedToBeSaved + } + + var pushOps = &PushOptions{} + for _, op := range ops { + if err := op(pushOps); err != nil { + return err + } + } + + if pushOps.Format != types.MediaType("") { + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return err + } + + if !pushOps.Format.IsIndex() { + return ErrUnknownMediaType(pushOps.Format) + } + + if pushOps.Format != mfest.MediaType { + h.ImageIndex = mutate.IndexMediaType(h.ImageIndex, pushOps.Format) + if err := h.Save(); err != nil { + return err + } + } + } + + layoutPath := filepath.Join(h.Options.XdgPath, h.Options.Reponame) + path, err := layout.FromPath(layoutPath) + if err != nil { + return err + } + + if h.ImageIndex, err = path.ImageIndex(); err != nil { + return err + } + + ref, err := name.ParseReference( + h.Options.Reponame, + name.WeakValidation, + name.Insecure, + ) + if err != nil { + return err + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return err + } + + var taggableIndex = NewTaggableIndex(mfest) + multiWriteTagables := map[name.Reference]remote.Taggable{ + ref: taggableIndex, + } + for _, tag := range pushOps.Tags { + multiWriteTagables[ref.Context().Tag(tag)] = taggableIndex + } + + // Note: It will only push IndexManifest, assuming all the Images it refers exists in registry + err = remote.MultiWrite( + multiWriteTagables, + remote.WithAuthFromKeychain(h.Options.KeyChain), + remote.WithTransport(GetTransport(pushOps.Insecure)), + ) + + if pushOps.Purge { + return h.Delete() + } + + return err } -type SaveIndexDiagnostic struct { - ImageIndexName string - Cause error +// Displays IndexManifest. +func (h *ManifestHandler) Inspect() (string, error) { + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return "", err + } + + if len(h.RemovedManifests) != 0 || len(h.Annotate.Instance) != 0 { + return "", ErrIndexNeedToBeSaved + } + + mfestBytes, err := json.MarshalIndent(mfest, "", " ") + if err != nil { + return "", err + } + + return string(mfestBytes), nil } -type SaveIndexError struct { - Errors []SaveIndexDiagnostic +// Remove Image/Index from ImageIndex. +// +// Accepts both Tags and Digests. +func (h *ManifestHandler) Remove(ref name.Reference) (err error) { + hash, err := parseReferenceToHash(ref, h.Options) + if err != nil { + return err + } + + if _, ok := h.Images[hash]; ok { + h.RemovedManifests = append(h.RemovedManifests, hash) + return nil + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + return err + } + + found := false + for _, d := range mfest.Manifests { + if d.Digest == hash { + found = true + break + } + } + + if !found { + return ErrNoImageOrIndexFoundWithGivenDigest(ref.Identifier()) + } + + h.RemovedManifests = append(h.RemovedManifests, hash) + return nil } -func (e SaveIndexError) Error() string { - var errors []string - for _, d := range e.Errors { - errors = append(errors, fmt.Sprintf("[%s: %s]", d.ImageIndexName, d.Cause.Error())) +// Remove ImageIndex from local filesystem if exists. +func (h *ManifestHandler) Delete() error { + layoutPath := filepath.Join(h.Options.XdgPath, h.Options.Reponame) + if _, err := os.Stat(layoutPath); err != nil { + return err } - return fmt.Sprintf("failed to write image to the following tags: %s", strings.Join(errors, ",")) + + return os.RemoveAll(layoutPath) +} + +func (h *ManifestHandler) getIndexURLs(hash v1.Hash) (urls []string, err error) { + idx, err := h.ImageIndex.ImageIndex(hash) + if err != nil { + return urls, err + } + + mfest, err := getIndexManifest(idx) + if err != nil { + return urls, err + } + + if mfest.Subject == nil { + mfest.Subject = &v1.Descriptor{} + } + + if len(mfest.Subject.URLs) == 0 { + return urls, ErrURLsUndefined(mfest.MediaType, hash.String()) + } + + return mfest.Subject.URLs, nil +} + +func (h *ManifestHandler) getImageURLs(hash v1.Hash) (urls []string, format types.MediaType, err error) { + if desc, ok := h.Images[hash]; ok { + if len(desc.URLs) == 0 { + return urls, desc.MediaType, ErrURLsUndefined(desc.MediaType, hash.String()) + } + + return desc.URLs, desc.MediaType, nil + } + + mfest, err := getIndexManifest(h.ImageIndex) + if err != nil { + // Return Non-Image and Non-Index mediaType + return urls, types.DockerConfigJSON, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + if len(desc.URLs) == 0 { + return urls, desc.MediaType, ErrURLsUndefined(desc.MediaType, hash.String()) + } + + return desc.URLs, desc.MediaType, nil + } + } + + return urls, mfest.MediaType, ErrNoImageOrIndexFoundWithGivenDigest(hash.String()) +} + +func (h *ManifestHandler) getIndexManifest(digest name.Digest) (mfest *v1.IndexManifest, err error) { + hash, err := v1.NewHash(digest.Identifier()) + if err != nil { + return + } + + if mfest, err = getIndexManifest(h.ImageIndex); err != nil { + return mfest, err + } + + for _, desc := range mfest.Manifests { + if desc.Digest == hash { + return &v1.IndexManifest{ + MediaType: desc.MediaType, + Subject: &desc, + }, nil + } + } + + return nil, ErrNoImageOrIndexFoundWithGivenDigest(hash.String()) } diff --git a/index/new.go b/index/new.go new file mode 100644 index 00000000..251d2056 --- /dev/null +++ b/index/new.go @@ -0,0 +1,42 @@ +package index + +import ( + "path/filepath" + + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/types" + + "github.com/buildpacks/imgutil" +) + +// NewIndex will return a New Empty ImageIndex that can be modified and saved to a registry +func NewIndex(repoName string, ops ...Option) (idx imgutil.ImageIndex, err error) { + var idxOps = &Options{} + ops = append(ops, WithRepoName(repoName)) + for _, op := range ops { + err = op(idxOps) + if err != nil { + return + } + } + + idxOptions := imgutil.IndexOptions{ + KeyChain: idxOps.keychain, + XdgPath: idxOps.xdgPath, + Reponame: idxOps.repoName, + InsecureRegistry: idxOps.insecure, + } + + layoutPath := filepath.Join(idxOps.xdgPath, idxOps.repoName) + switch idxOps.format { + case types.DockerManifestList: + idx = imgutil.NewManifestHandler(imgutil.NewEmptyDockerIndex(), idxOptions) + _, err = layout.Write(layoutPath, imgutil.NewEmptyDockerIndex()) + default: + idx = imgutil.NewManifestHandler(empty.Index, idxOptions) + _, err = layout.Write(layoutPath, empty.Index) + } + + return idx, err +} diff --git a/index/new_test.go b/index/new_test.go new file mode 100644 index 00000000..bcd745fc --- /dev/null +++ b/index/new_test.go @@ -0,0 +1,46 @@ +package index_test + +import ( + "os" + "testing" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestRemoteNew(t *testing.T) { + spec.Run(t, "RemoteNew", testRemoteNew, spec.Sequential(), spec.Report(report.Terminal{})) +} + +func testRemoteNew(t *testing.T, when spec.G, it spec.S) { + when("#NewIndex", func() { + var ( + idx imgutil.ImageIndex + err error + xdgPath = "xdgPath" + ) + it.After(func() { + h.AssertNil(t, os.RemoveAll(xdgPath)) + }) + it("should have expected indexOptions", func() { + idx, err = index.NewIndex("repo/name", index.WithInsecure(true), index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertEq(t, idx.(*imgutil.ManifestHandler).Options.InsecureRegistry, true) + }) + it("should return an error when invalid repoName is passed", func() { + idx, err = index.NewIndex("invalid/repoName", index.WithInsecure(true), index.WithXDGRuntimePath(xdgPath)) + h.AssertNotEq(t, err, nil) + }) + it("should return ManifestHanler", func() { + idx, err = index.NewIndex("repo/name", index.WithInsecure(true), index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + _, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + }) + }) +} diff --git a/index/options.go b/index/options.go new file mode 100644 index 00000000..aff5e3d6 --- /dev/null +++ b/index/options.go @@ -0,0 +1,87 @@ +package index + +import ( + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type Option func(*Options) error + +type Options struct { + keychain authn.Keychain + xdgPath, repoName string + insecure bool + format types.MediaType +} + +func (o *Options) Keychain() authn.Keychain { + return o.keychain +} + +func (o *Options) XDGRuntimePath() string { + return o.xdgPath +} + +func (o *Options) RepoName() string { + return o.repoName +} + +func (o *Options) Insecure() bool { + return o.insecure +} + +func (o *Options) Format() types.MediaType { + return o.format +} + +// Fetch Index from registry with keychain +func WithKeychain(keychain authn.Keychain) Option { + return func(o *Options) error { + o.keychain = keychain + return nil + } +} + +// Save the Index to the '`xdgPath`/manifests' +func WithXDGRuntimePath(xdgPath string) Option { + return func(o *Options) error { + o.xdgPath = xdgPath + return nil + } +} + +// Create a local index with repoName/Reference +func WithRepoName(repoName string) Option { + return func(o *Options) error { + if o.insecure { + _, err := name.ParseReference(repoName, name.Insecure, name.WeakValidation) + if err != nil { + return err + } + } else { + _, err := name.ParseReference(repoName, name.WeakValidation) + if err != nil { + return err + } + } + o.repoName = repoName + return nil + } +} + +// If true, pulls images from insecure registry +func WithInsecure(insecure bool) Option { + return func(o *Options) error { + o.insecure = insecure + return nil + } +} + +// Create the image index with the following format +func WithFormat(format types.MediaType) Option { + return func(o *Options) error { + o.format = format + return nil + } +} diff --git a/index/options_test.go b/index/options_test.go new file mode 100644 index 00000000..e2835a26 --- /dev/null +++ b/index/options_test.go @@ -0,0 +1,70 @@ +package index_test + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil/index" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestRemoteOptions(t *testing.T) { + spec.Run(t, "RemoteNew", testRemoteOptions, spec.Sequential(), spec.Report(report.Terminal{})) +} + +func testRemoteOptions(t *testing.T, when spec.G, it spec.S) { + var ( + ops = &index.Options{} + opts = []index.Option(nil) + ) + when("#NewIndex", func() { + it.Before(func() { + ops = &index.Options{} + opts = []index.Option(nil) + }) + it("should have expected xdgpath value", func() { + opts = append(opts, index.WithXDGRuntimePath("xdgPath")) + for _, op := range opts { + op(ops) + } + + h.AssertEq(t, ops.XDGRuntimePath(), "xdgPath") + }) + it("should return an error when invalid repoName is passed", func() { + opts = append(opts, index.WithRepoName("repo/name")) + for _, op := range opts { + h.AssertNil(t, op(ops)) + } + + h.AssertEq(t, ops.RepoName(), "repo/name") + }) + it("should return an error when index with the given repoName doesn't exists", func() { + opts = append(opts, index.WithRepoName("repoName")) + for _, op := range opts { + err := op(ops) + h.AssertNotEq(t, err, nil) + } + + h.AssertEq(t, ops.RepoName(), "") + }) + it("should have expected insecure value", func() { + opts = append(opts, index.WithInsecure(true)) + for _, op := range opts { + op(ops) + } + + h.AssertEq(t, ops.Insecure(), true) + }) + it("should have expected format value", func() { + opts = append(opts, index.WithFormat(types.DockerManifestList)) + for _, op := range opts { + op(ops) + } + + h.AssertEq(t, ops.Format(), types.DockerManifestList) + }) + }) +} diff --git a/index_test.go b/index_test.go new file mode 100644 index 00000000..ab311be1 --- /dev/null +++ b/index_test.go @@ -0,0 +1,3723 @@ +package imgutil_test + +import ( + "fmt" + "os" + "runtime" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" + "github.com/buildpacks/imgutil/layout" + "github.com/buildpacks/imgutil/local" + "github.com/buildpacks/imgutil/remote" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestIndex(t *testing.T) { + spec.Run(t, "Index", testIndex, spec.Sequential(), spec.Report(report.Terminal{})) +} + +func testIndex(t *testing.T, when spec.G, it spec.S) { + var ( + xdgPath = "xdgPath" + ) + when("#ManifestHandler", func() { + it.After(func() { + err := os.RemoveAll(xdgPath) + h.AssertNil(t, err) + }) + when("#OS", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.OS(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #OS requested", func() { + digest, err := name.NewDigest("busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + os, err := idx.OS(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, os, "") + }) + it("should return latest OS when os of the given digest annotated", func() { + digest, err := name.NewDigest("busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + hash: { + Platform: &v1.Platform{ + OS: "some-os", + }, + }, + }, + }, + } + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + }) + it("should return an error when an image with the given digest doesn't exists", func() { + digest, err := name.NewDigest("busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + os, err := idx.OS(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, os, "") + }) + it("should return expected os when os is not annotated before", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + }) + }) + when("#SetOS", func() { + it("should return an error when invalid digest is provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetOS(digest, "some-os") + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #SetOS requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetOS(digest, "some-os") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetOS for the given digest when image/index exists", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetOS(digest, "some-os") + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + }) + it("it should return an error when image/index with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetOS(digest, "some-os") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("#Architecture", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.Architecture(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #Architecture requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + os, err := idx.Architecture(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, os, "") + }) + it("should return latest Architecture when arch of the given digest annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + hash: { + Platform: &v1.Platform{ + Architecture: "some-arch", + }, + }, + }, + }, + } + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "some-arch") + }) + it("should return an error when an image with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + arch, err := idx.Architecture(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, arch, "") + }) + it("should return expected Architecture when arch is not annotated before", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("busybox:1.36-musl", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + }) + }) + when("#SetArchitecture", func() { + it("should return an error when invalid digest is provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetArchitecture(digest, "some-arch") + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #SetArchitecture requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetArchitecture(digest, "some-arch") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetArchitecture for the given digest when image/index exists", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetArchitecture(digest, "some-arch") + h.AssertNil(t, err) + + os, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-arch") + }) + it("it should return an error when image/index with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetArchitecture(digest, "some-arch") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("#Variant", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.Architecture(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #Variant requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + variant, err := idx.Variant(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, variant, "") + }) + it("should return latest Variant when variant of the given digest annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + hash: { + Platform: &v1.Platform{ + Variant: "some-variant", + }, + }, + }, + }, + } + + variant, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, variant, "some-variant") + }) + it("should return an error when an image with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + arch, err := idx.Variant(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, arch, "") + }) + it("should return expected Variant when arch is not annotated before", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("busybox:1.36-musl", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + arch, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "v6") + }) + }) + when("#SetVariant", func() { + it("should return an error when invalid digest is provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetVariant(digest, "some-variant") + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #SetVariant requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetVariant(digest, "some-variant") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetVariant for the given digest when image/index exists", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetVariant(digest, "some-variant") + h.AssertNil(t, err) + + os, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-variant") + }) + it("it should return an error when image/index with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetVariant(digest, "some-variant") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("#OSVersion", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.OSVersion(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #OSVersion requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + osVersion, err := idx.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + }) + it("should return latest OSVersion when osVersion of the given digest annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + hash: { + Platform: &v1.Platform{ + OSVersion: "some-osVersion", + }, + }, + }, + }, + } + + variant, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, variant, "some-osVersion") + }) + it("should return an error when an image with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + osVersion, err := idx.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + }) + it("should return expected OSVersion when arch is not annotated before", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("busybox:1.36-musl", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + err = idx.SetOSVersion(digest, "some-osVersion") + h.AssertNil(t, err) + + osVersion, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, osVersion, "some-osVersion") + }) + }) + when("#SetOSVersion", func() { + it("should return an error when invalid digest is provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetOSVersion(digest, "some-osVersion") + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if a removed image/index's #SetOSVersion requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetOSVersion(digest, "some-osVersion") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetOSVersion for the given digest when image/index exists", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetOSVersion(digest, "some-osVersion") + h.AssertNil(t, err) + + os, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-osVersion") + }) + it("it should return an error when image/index with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetOSVersion(digest, "some-osVersion") + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("#Features", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.Features(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #Features is requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + features, err := idx.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + }) + it("should return annotated Features when Features of the image/index is annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + hash: { + Platform: &v1.Platform{ + Features: []string{"some-features"}, + }, + }, + }, + }, + } + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, []string{"some-features"}) + }) + it("should return error if the image/index with the given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + features, err := idx.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + }) + it("should return expected Features of the given image/index when image/index is not annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("busybox:1.36-musl", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + err = idx.SetFeatures(digest, []string{"some-features"}) + h.AssertNil(t, err) + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, []string{"some-features"}) + }) + }) + when("#SetFeatures", func() { + it("should return an error when an invalid digest is provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetFeatures(digest, []string{"some-features"}) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #SetFeatures is requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetFeatures(digest, []string{"some-features"}) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetFeatures when the given digest is image/index", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetFeatures(digest, []string{"some-features"}) + h.AssertNil(t, err) + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, []string{"some-features"}) + }) + it("should return an error when no image/index with the given digest exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetFeatures(digest, []string{"some-features"}) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("#OSFeatures", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.OSFeatures(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #OSFeatures is requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + osFeatures, err := idx.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + }) + it("should return annotated OSFeatures when OSFeatures of the image/index is annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + hash: { + Platform: &v1.Platform{ + OSFeatures: []string{"some-osFeatures"}, + }, + }, + }, + }, + } + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-osFeatures"}) + }) + it("should return the OSFeatures if the image/index with the given digest exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + osFeatures, err := idx.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + }) + it("should return expected OSFeatures of the given image when image/index is not annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("busybox:1.36-musl", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + err = idx.SetOSFeatures(digest, []string{"some-osFeatures"}) + h.AssertNil(t, err) + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-osFeatures"}) + }) + }) + when("#SetOSFeatures", func() { + it("should return an error when an invalid digest is provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetFeatures(digest, []string{"some-osFeatures"}) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #SetOSFeatures is requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetOSFeatures(digest, []string{"some-osFeatures"}) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetOSFeatures when the given digest is image/index", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetOSFeatures(digest, []string{"some-osFeatures"}) + h.AssertNil(t, err) + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-osFeatures"}) + }) + it("should return an error when no image/index with the given digest exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetOSFeatures(digest, []string{"some-osFeatures"}) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("docker manifest list", func() { + when("#Annotations", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.OSFeatures(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #Annotations is requested", func() { + digest, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: imgutil.NewEmptyDockerIndex(), + RemovedManifests: []v1.Hash{ + hash, + }, + } + + annotations, err := idx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should return annotated Annotations when Annotations of the image/index is annotated", func() { + digest, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should return the Annotations if the image/index with the given digest exists", func() { + digest, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: imgutil.NewEmptyDockerIndex(), + } + + annotations, err := idx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should return expected Annotations of the given image/index when image/index is not annotated", func() { + digest, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("alpine:3.19.0", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + }) + when("#SetAnnotations", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if the image/index is removed", func() { + digest, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: imgutil.NewEmptyDockerIndex(), + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetAnnotations when an image/index with the given digest exists", func() { + idx, err := remote.NewIndex( + "alpine:latest", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.ImageIndex.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfest, nil) + + hash := mfest.Manifests[0].Digest + digest, err := name.NewDigest("alpine@" + hash.String()) + h.AssertNil(t, err) + + err = imgIdx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + annotations, err := imgIdx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should return an error if the manifest with the given digest is neither image nor index", func() { + digest, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: imgutil.NewEmptyDockerIndex(), + } + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + }) + when("oci image index", func() { + when("#Annotations", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.OSFeatures(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #Annotations is requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + annotations, err := idx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should return annotated Annotations when Annotations of the image/index is annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertNil(t, err) + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should return the Annotations if the image/index with the given digest exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + annotations, err := idx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should return expected Annotations of the given image when image/index is not annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("busybox:1.36-musl", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertNil(t, err) + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + }) + when("#SetAnnotations", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error if the image/index is removed", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetAnnotations when an image/index with the given digest exists", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertNil(t, err) + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should return an error if the manifest with the given digest is neither image nor index", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetAnnotations(digest, map[string]string{ + "some-key": "some-value", + }) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + }) + when("#URLs", func() { + it("should return an error when invalid digest provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + _, err := idx.URLs(digest) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #URLs is requested", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + urls, err := idx.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + }) + it("should return annotated URLs when URLs of the image/index is annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + hash: { + URLs: []string{ + "some-urls", + }, + }, + }, + }, + } + + urls, err := idx.URLs(digest) + h.AssertNil(t, err) + h.AssertEq(t, urls, []string{ + "some-urls", + }) + }) + it("should return the URLs if the image/index with the given digest exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + urls, err := idx.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + }) + it("should return expected URLs of the given image when image/index is not annotated", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx, err := remote.NewIndex("busybox:1.36-musl", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, v1.ImageIndex(nil)) + + err = idx.SetURLs(digest, []string{ + "some-urls", + }) + h.AssertNil(t, err) + + urls, err := idx.URLs(digest) + h.AssertNil(t, err) + h.AssertEq(t, urls, []string{ + "some-urls", + }) + }) + }) + when("#SetURLs", func() { + it("should return an error when an invalid digest is provided", func() { + digest := name.Digest{} + idx := imgutil.ManifestHandler{} + err := idx.SetURLs(digest, []string{"some-urls"}) + h.AssertEq(t, err.Error(), fmt.Errorf(`cannot parse hash: "%s"`, digest.Identifier()).Error()) + }) + it("should return an error when a removed manifest's #SetURLs is requested", func() { + digest, err := name.NewDigest("busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + hash, err := v1.NewHash(digest.Identifier()) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + hash, + }, + } + + err = idx.SetURLs(digest, []string{ + "some-urls", + }) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should SetOSFeatures when the given digest is image/index", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.SetURLs(digest, []string{ + "some-urls", + }) + h.AssertNil(t, err) + + urls, err := idx.URLs(digest) + h.AssertNil(t, err) + h.AssertEq(t, urls, []string{ + "some-urls", + }) + }) + it("should return an error when no image/index with the given digest exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.SetURLs(digest, []string{ + "some-urls", + }) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("#Add", func() { + it("should return an error when the image/index with the given reference doesn't exists", func() { + _, err := remote.NewIndex( + "unknown/index", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertEq(t, err.Error(), "GET https://index.docker.io/v2/unknown/index/manifests/latest: UNAUTHORIZED: authentication required; [map[Action:pull Class: Name:unknown/index Type:repository]]") + }) + when("platform specific", func() { + it("should add platform specific image", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine:3.19", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithOS("linux"), + imgutil.WithArchitecture("amd64"), + ) + h.AssertNil(t, err) + + index := idx.(*imgutil.ManifestHandler) + + hashes := make([]v1.Hash, 0, len(index.Images)) + for h2 := range index.Images { + hashes = append(hashes, h2) + } + h.AssertEq(t, len(hashes), 1) + + digest, err := name.NewDigest("alpine@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + os, err := index.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := index.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err := index.Variant(digest) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := index.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := index.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := index.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := index.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := index.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should add annotations when WithAnnotations used for oci", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "busybox:1.36-musl", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithOS("linux"), + imgutil.WithArchitecture("amd64"), + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest("busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertNil(t, err) + + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should not add annotations when WithAnnotations used for docker", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + idx, err := local.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine:3.19.0", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithOS("linux"), + imgutil.WithArchitecture("amd64"), + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest("alpine@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd", name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + annotations, err := idx.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + }) + when("target specific", func() { + it("should add target specific image", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine:3.19.0", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add(ref) + h.AssertNil(t, err) + + index := idx.(*imgutil.ManifestHandler) + hashes := make([]v1.Hash, 0, len(index.Images)) + for h2 := range index.Images { + hashes = append(hashes, h2) + } + h.AssertEq(t, len(hashes), 1) + + hash := hashes[0] + digest, err := name.NewDigest("alpine@"+hash.String(), name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + os, err := index.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, runtime.GOOS) + + arch, err := index.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, runtime.GOARCH) + + variant, err := index.Variant(digest) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := index.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := index.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := index.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := index.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := index.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should add annotations when WithAnnotations used for oci", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "busybox:1.36-musl", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + index := idx.(*imgutil.ManifestHandler) + hashes := make([]v1.Hash, 0, len(index.Images)) + for h2 := range index.Images { + hashes = append(hashes, h2) + } + h.AssertEq(t, len(hashes), 1) + + hash := hashes[0] + digest, err := name.NewDigest("busybox@"+hash.String(), name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + os, err := index.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, runtime.GOOS) + + arch, err := index.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, runtime.GOARCH) + + variant, err := index.Variant(digest) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.OCIImageIndex, digest.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := index.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := index.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := index.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := index.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := index.Annotations(digest) + h.AssertNil(t, err) + + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should not add annotations when WithAnnotations used for docker", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + idx, err := local.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine:latest", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + index := idx.(*imgutil.ManifestHandler) + hashes := make([]v1.Hash, 0, len(index.Images)) + for h2 := range index.Images { + hashes = append(hashes, h2) + } + h.AssertEq(t, len(hashes), 1) + + hash := hashes[0] + digest, err := name.NewDigest("alpine@"+hash.String(), name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + os, err := index.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, runtime.GOOS) + + arch, err := index.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, runtime.GOARCH) + + annotations, err := index.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + }) + when("image specific", func() { + it("should not change the digest of the image when added", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add(ref) + h.AssertNil(t, err) + + index := idx.(*imgutil.ManifestHandler) + hashes := make([]v1.Hash, 0, len(index.Images)) + for h2 := range index.Images { + hashes = append(hashes, h2) + } + + h.AssertEq(t, len(hashes), 1) + hash := hashes[0] + digest, err := name.NewDigest( + "alpine@"+hash.String(), + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + os, err := index.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := index.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err := index.Variant(digest) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := index.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := index.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := index.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := index.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := index.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should annotate the annotations when Annotations provided for oci", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + index := idx.(*imgutil.ManifestHandler) + ref, err := name.ParseReference( + "busybox@sha256:648143a312f16e5b5a6f64dfa4024a281fb4a30467500ca8b0091a9984f1c751", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = index.Add( + ref, + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + hashes := make([]v1.Hash, 0, len(index.Images)) + for h2 := range index.Images { + hashes = append(hashes, h2) + } + + h.AssertEq(t, len(hashes), 1) + hash := hashes[0] + digest, err := name.NewDigest("busybox@"+hash.String(), name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + os, err := index.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := index.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm64") + + variant, err := index.Variant(digest) + h.AssertEq(t, err, nil) + h.AssertEq(t, variant, "v8") + + osVersion, err := index.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := index.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := index.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := index.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := index.Annotations(digest) + h.AssertNil(t, err) + + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should not annotate the annotations when Annotations provided for docker", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + idx, err := local.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + index := idx.(*imgutil.ManifestHandler) + hashes := make([]v1.Hash, 0, len(index.Images)) + for h2 := range index.Images { + hashes = append(hashes, h2) + } + h.AssertEq(t, len(hashes), 1) + + hash := hashes[0] + digest, err := name.NewDigest("alpine@"+hash.String(), name.WeakValidation, name.Insecure) + h.AssertNil(t, err) + + os, err := index.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := index.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err := index.Variant(digest) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := index.OSVersion(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := index.Features(digest) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := index.OSFeatures(digest) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := index.URLs(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := index.Annotations(digest) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest.Identifier()).Error()) + + v, ok := annotations["some-key"] + h.AssertEq(t, ok, false) + h.AssertEq(t, v, "") + }) + }) + when("index specific", func() { + it("should add all the images of the given reference", func() { + _, err := index.NewIndex( + "some/image:tag", + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.DockerManifestList), + ) + h.AssertNil(t, err) + + idx, err := local.NewIndex( + "some/image:tag", + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine:3.19.0", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/amd64 + digest1, err := name.NewDigest( + "alpine@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux arm/v6 + digest2, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add(ref, imgutil.WithAll(true)) + h.AssertNil(t, err) + + os, err := idx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := idx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err := idx.Variant(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := idx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := idx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := idx.OSFeatures(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := idx.URLs(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := idx.Annotations(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + + os, err = idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = idx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + + variant, err = idx.Variant(digest2) + h.AssertNil(t, err) + h.AssertEq(t, variant, "v6") + + osVersion, err = idx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = idx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = idx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = idx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = idx.Annotations(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should not ignore WithAnnotations for oci", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "busybox:1.36-musl", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + digest1, err := name.NewDigest( + "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + digest2, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + imgutil.WithAll(true), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := idx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err := idx.Variant(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := idx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := idx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := idx.OSFeatures(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := idx.URLs(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := idx.Annotations(digest1) + h.AssertNil(t, err) + + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + + os, err = idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = idx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + + arch, err = idx.Variant(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "v6") + + osVersion, err = idx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = idx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = idx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = idx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = idx.Annotations(digest2) + h.AssertNil(t, err) + + v, ok = annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should ignore WithAnnotations for docker", func() { + _, err := index.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + idx, err := local.NewIndex("some/image:tag", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.DockerManifestList)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "alpine:3.19.0", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + digest1, err := name.NewDigest( + "alpine@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + digest2, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + imgutil.WithAll(true), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := idx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err := idx.Variant(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := idx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := idx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := idx.OSFeatures(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := idx.URLs(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := idx.Annotations(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + + os, err = idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = idx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + + variant, err = idx.Variant(digest2) + h.AssertNil(t, err) + h.AssertEq(t, variant, "v6") + + osVersion, err = idx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = idx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = idx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = idx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = idx.Annotations(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + }) + }) + when("#Save", func() { + it("should save the index", func() { + idx, err := remote.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + _, err = local.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + }) + it("should save the annotated index", func() { + idx, err := remote.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/index@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd") + h.AssertNil(t, err) + + err = idx.SetOS(digest, "some-os") + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + // locally saved image should also work as expected + indx, err := local.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err := indx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + + err = indx.SetOS(digest, "some-os") + h.AssertNil(t, err) + + err = indx.SetArchitecture(digest, "something") + h.AssertNil(t, err) + + err = indx.SetVariant(digest, "something") + h.AssertNil(t, err) + + err = indx.SetOSVersion(digest, "something") + h.AssertNil(t, err) + + err = indx.SetFeatures(digest, []string{"some-features"}) + h.AssertNil(t, err) + + err = indx.SetOSFeatures(digest, []string{"some-osFeatures"}) + h.AssertNil(t, err) + + err = indx.SetURLs(digest, []string{"some-urls"}) + h.AssertNil(t, err) + + err = indx.SetAnnotations(digest, map[string]string{"some-key": "some-value"}) + h.AssertNil(t, err) + + h.AssertNil(t, indx.Save()) + + idx, err = local.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err = idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + }) + it("should save the added yet annotated images", func() { + idx, err := remote.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + digest, err := name.NewDigest("busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", name.WeakValidation) + h.AssertNil(t, err) + + err = idx.Add(digest) + h.AssertNil(t, err) + + err = idx.SetOS(digest, "some-os") + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = local.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + }) + it("should save all added images", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx1, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox:1.36-musl", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx1.Add(ref, imgutil.WithAll(true)) + h.AssertNil(t, err) + + ii1, ok := idx1.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + hashes := make([]v1.Hash, 0, len(ii1.Images)) + for h2 := range ii1.Images { + hashes = append(hashes, h2) + } + h.AssertEq(t, len(hashes), 8) + + err = idx1.Save() + h.AssertNil(t, err) + + idx2, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii2, ok := idx2.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii2.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), 8) + + // linux/amd64 + imgRefStr := "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34" + digest, err := name.NewDigest(imgRefStr, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + os, err := ii2.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + }) + it("should save all added images with annotations", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx1, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox:1.36-musl", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx1.Add( + ref, + imgutil.WithAll(true), + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + ii1, ok := idx1.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + keys := make([]v1.Hash, 0, len(ii1.Images)) + for h2 := range ii1.Images { + keys = append(keys, h2) + } + h.AssertEq(t, len(keys), 8) + + err = idx1.Save() + h.AssertNil(t, err) + + idx2, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii2, ok := idx2.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii2.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), len(keys)) + + // linux/amd64 + var imgRefStr1 = "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34" + h.AssertNotEq(t, imgRefStr1, "") + digest1, err := name.NewDigest(imgRefStr1, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + // linux/arm64 + var imgRefStr2 = "busybox@sha256:648143a312f16e5b5a6f64dfa4024a281fb4a30467500ca8b0091a9984f1c751" + h.AssertNotEq(t, imgRefStr2, "") + digest2, err := name.NewDigest(imgRefStr2, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + os, err := ii2.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := ii2.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + annos, err := ii2.Annotations(digest1) + h.AssertNil(t, err) + + v, ok := annos["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + + os, err = ii2.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = ii2.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm64") + + annos, err = ii2.Annotations(digest2) + h.AssertNil(t, err) + + v, ok = annos["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should save platform specific added image", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox:1.36-musl", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx.Add(ref) + h.AssertNil(t, err) + + ii, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + keys := make([]v1.Hash, 0, len(ii.Images)) + for h2 := range ii.Images { + keys = append(keys, h2) + } + h.AssertEq(t, len(keys), 1) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii, ok = idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), len(keys)) + + imgRefStr := "busybox@" + mfestSaved.Manifests[0].Digest.String() + digest, err := name.NewDigest(imgRefStr, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + os, err := ii.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, runtime.GOOS) + + arch, err := ii.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, runtime.GOARCH) + }) + it("should save platform specific added image with annotations", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox:1.36-musl", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx.Add(ref, imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + })) + h.AssertNil(t, err) + + ii, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + keys := make([]v1.Hash, 0, len(ii.Images)) + for h2 := range ii.Images { + keys = append(keys, h2) + } + h.AssertEq(t, len(keys), 1) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii, ok = idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), len(keys)) + + imgRefStr := "busybox@" + mfestSaved.Manifests[0].Digest.String() + digest, err := name.NewDigest(imgRefStr, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + os, err := ii.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, runtime.GOOS) + + arch, err := ii.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, runtime.GOARCH) + + annos, err := ii.Annotations(digest) + h.AssertNil(t, err) + + v, ok := annos["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should save target specific added images", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox:1.36-musl", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx.Add(ref, imgutil.WithOS("linux"), imgutil.WithArchitecture("amd64")) + h.AssertNil(t, err) + + ii, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + keys := make([]v1.Hash, 0, len(ii.Images)) + for h2 := range ii.Images { + keys = append(keys, h2) + } + h.AssertEq(t, len(keys), 1) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii, ok = idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), len(keys)) + + // linux/amd64 + imgRefStr := "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34" + digest, err := name.NewDigest(imgRefStr, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + os, err := ii.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := ii.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + }) + it("should save target specific added images with Annotations", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox:1.36-musl", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx.Add( + ref, + imgutil.WithOS("linux"), + imgutil.WithArchitecture("amd64"), + imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + }), + ) + h.AssertNil(t, err) + + ii, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + keys := make([]v1.Hash, 0, len(ii.Images)) + for h2 := range ii.Images { + keys = append(keys, h2) + } + h.AssertEq(t, len(keys), 1) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii, ok = idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), len(keys)) + + // linux/amd64 + var imgRefStr1 = "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34" + digest, err := name.NewDigest(imgRefStr1, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + os, err := ii.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := ii.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + annos, err := ii.Annotations(digest) + h.AssertNil(t, err) + + v, ok := annos["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should save single added image", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx.Add(ref) + h.AssertNil(t, err) + + ii, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + keys := make([]v1.Hash, 0, len(ii.Images)) + for h2 := range ii.Images { + keys = append(keys, h2) + } + h.AssertEq(t, len(keys), 1) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii, ok = idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), 1) + + // linux/amd64 + imgRefStr := "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34" + digest, err := name.NewDigest(imgRefStr, name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + os, err := ii.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := ii.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + }) + it("should save single added image with annotations", func() { + _, err := index.NewIndex( + "pack/imgutil", + index.WithXDGRuntimePath(xdgPath), + index.WithFormat(types.OCIImageIndex), + ) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ref, err := name.ParseReference("busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + err = idx.Add(ref, imgutil.WithAnnotations(map[string]string{ + "some-key": "some-value", + })) + h.AssertNil(t, err) + + ii, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + keys := make([]v1.Hash, 0, len(ii.Images)) + for h2 := range ii.Images { + keys = append(keys, h2) + } + h.AssertEq(t, len(keys), 1) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex("pack/imgutil", index.WithXDGRuntimePath(xdgPath)) + h.AssertNil(t, err) + + ii, ok = idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfestSaved, err := ii.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfestSaved, nil) + h.AssertEq(t, len(mfestSaved.Manifests), 1) + + digest, ok := ref.(name.Digest) + h.AssertEq(t, ok, true) + + os, err := ii.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := ii.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + annos, err := ii.Annotations(digest) + h.AssertNil(t, err) + v, ok := annos["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + }) + it("should save the annotated images", func() { + idx, err := remote.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + // linux/arm/v6 + digest1, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/amd64 + digest2, err := name.NewDigest( + "alpine@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.SetOS(digest1, "some-os") + h.AssertNil(t, err) + + err = idx.SetArchitecture(digest1, "some-arch") + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = local.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + + arch, err := idx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "some-arch") + + variant, err := idx.Variant(digest1) + h.AssertNil(t, err) + h.AssertEq(t, variant, "v6") + + osVersion, err := idx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := idx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := idx.OSFeatures(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := idx.URLs(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := idx.Annotations(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + + os, err = idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = idx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err = idx.Variant(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err = idx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = idx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = idx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = idx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = idx.Annotations(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should not save annotations for docker image/index", func() { + idx, err := remote.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + // linux/arm/v6 + digest1, err := name.NewDigest( + "alpine@sha256:45eeb55d6698849eb12a02d3e9a323e3d8e656882ef4ca542d1dda0274231e84", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/amd64 + digest2, err := name.NewDigest( + "alpine@sha256:13b7e62e8df80264dbb747995705a986aa530415763a6c58f84a3ca8af9a5bcd", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.SetAnnotations(digest1, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + err = idx.(*imgutil.ManifestHandler).Save() + h.AssertNil(t, err) + + idx, err = local.NewIndex( + "alpine:3.19.0", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := idx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + + variant, err := idx.Variant(digest1) + h.AssertNil(t, err) + h.AssertEq(t, variant, "v6") + + osVersion, err := idx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := idx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := idx.OSFeatures(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := idx.URLs(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := idx.Annotations(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest1.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + + os, err = idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = idx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err = idx.Variant(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err = idx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = idx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = idx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = idx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = idx.Annotations(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrAnnotationsUndefined(types.DockerManifestList, digest2.Identifier()).Error()) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should save the annotated annotations fields", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + // linux/amd64 + digest1, err := name.NewDigest( + "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/arm/v6 + digest2, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.SetAnnotations(digest1, map[string]string{ + "some-key": "some-value", + }) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := idx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err := idx.Variant(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err := idx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := idx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := idx.OSFeatures(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := idx.URLs(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := idx.Annotations(digest1) + h.AssertNil(t, err) + v, ok := annotations["some-key"] + h.AssertEq(t, ok, true) + h.AssertEq(t, v, "some-value") + + os, err = idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = idx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + + variant, err = idx.Variant(digest2) + h.AssertNil(t, err) + h.AssertEq(t, variant, "v6") + + osVersion, err = idx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = idx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = idx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = idx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = idx.Annotations(digest2) + h.AssertNil(t, err) + h.AssertNotEq(t, annotations, map[string]string{}) + }) + it("should save the annotated urls", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + // linux/arm/v6 + digest1, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/amd64 + digest2, err := name.NewDigest( + "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.SetURLs(digest1, []string{ + "some-urls", + }) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := idx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + + variant, err := idx.Variant(digest1) + h.AssertNil(t, err) + h.AssertEq(t, variant, "v6") + + osVersion, err := idx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := idx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := idx.OSFeatures(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err := idx.URLs(digest1) + h.AssertNil(t, err) + h.AssertEq(t, urls, []string{ + "some-urls", + }) + + annotations, err := idx.Annotations(digest1) + h.AssertNil(t, err) + h.AssertNotEq(t, annotations, map[string]string(nil)) + + os, err = idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = idx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err = idx.Variant(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err = idx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = idx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = idx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = idx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = idx.Annotations(digest2) + h.AssertNil(t, err) + h.AssertNotEq(t, annotations, map[string]string(nil)) + }) + it("should save annotated osFeatures", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + // linux/arm/v6 + digest1, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/amd64 + digest2, err := name.NewDigest( + "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.SetOSFeatures(digest1, []string{ + "some-osFeatures", + }) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + layoutIdx, err := layout.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + os, err := layoutIdx.OS(digest1) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err := layoutIdx.Architecture(digest1) + h.AssertNil(t, err) + h.AssertEq(t, arch, "arm") + + variant, err := layoutIdx.Variant(digest1) + h.AssertNil(t, err) + h.AssertEq(t, variant, "v6") + + osVersion, err := layoutIdx.OSVersion(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err := layoutIdx.Features(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest1.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err := layoutIdx.OSFeatures(digest1) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{ + "some-osFeatures", + }) + + urls, err := layoutIdx.URLs(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err := layoutIdx.Annotations(digest1) + h.AssertNil(t, err) + h.AssertNotEq(t, annotations, map[string]string(nil)) + + os, err = layoutIdx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + + arch, err = layoutIdx.Architecture(digest2) + h.AssertNil(t, err) + h.AssertEq(t, arch, "amd64") + + variant, err = layoutIdx.Variant(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrVariantUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, variant, "") + + osVersion, err = layoutIdx.OSVersion(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSVersionUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osVersion, "") + + features, err = layoutIdx.Features(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, features, []string(nil)) + + osFeatures, err = layoutIdx.OSFeatures(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrOSFeaturesUndefined(types.OCIImageIndex, digest2.Identifier()).Error()) + h.AssertEq(t, osFeatures, []string(nil)) + + urls, err = layoutIdx.URLs(digest2) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest2.Identifier()).Error()) + h.AssertEq(t, urls, []string(nil)) + + annotations, err = layoutIdx.Annotations(digest2) + h.AssertNil(t, err) + h.AssertNotEq(t, annotations, map[string]string(nil)) + }) + it("should remove the images/indexes from save's output", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + // linux/arm/v6 + digest1, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/amd64 + digest2, err := name.NewDigest( + "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.Remove(digest1) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + _, err = idx.OS(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + + os, err := idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "linux") + }) + it("should set the Annotate and RemovedManifests to empty slice", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + // linux/arm/v6 + digest1, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + // linux/amd64 + digest2, err := name.NewDigest( + "busybox@sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.Remove(digest1) + h.AssertNil(t, err) + + err = idx.SetOS(digest2, "some-os") + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + idx, err = layout.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + _, err = idx.OS(digest1) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest1.Identifier()).Error()) + + os, err := idx.OS(digest2) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + }) + it("should return an error", func() { + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + {}: { + MediaType: types.DockerConfigJSON, + }, + }, + }, + Options: imgutil.IndexOptions{ + Reponame: "alpine:latest", + XdgPath: xdgPath, + }, + } + + err := idx.Save() + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(":").Error()) + }) + }) + when("#Push", func() { + it("should return an error when index is not saved", func() { + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + Annotate: imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{ + {}: { + MediaType: types.DockerConfigJSON, + }, + }, + }, + } + + err := idx.Push() + h.AssertEq(t, err.Error(), imgutil.ErrIndexNeedToBeSaved.Error()) + }) + // FIXME: should need to create a mock to push images and indexes + it("should push index to registry", func() {}) + it("should push with insecure registry when WithInsecure used", func() {}) + it("should delete local image index", func() {}) + it("should annoate index media type before pushing", func() {}) + }) + when("#Inspect", func() { + it("should return an error", func() { + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + RemovedManifests: []v1.Hash{ + {}, + }, + } + + mfest, err := idx.Inspect() + h.AssertNotEq(t, err, nil) + h.AssertEq(t, mfest, "") + }) + it("should return index manifest", func() { + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + mfest, err := idx.Inspect() + h.AssertNil(t, err) + h.AssertEq(t, mfest, `{ +"schemaVersion": 2, +"mediaType": "application/vnd.oci.image.index.v1+json", +"manifests": [] +}`) + }) + }) + when("#Remove", func() { + it("should return error when invalid digest provided", func() { + digest := name.Digest{} + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err := idx.Remove(digest) + h.AssertEq(t, err.Error(), fmt.Sprintf(`cannot parse hash: "%s"`, digest.Identifier())) + }) + it("should return an error when manifest with given digest doesn't exists", func() { + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + idx := imgutil.ManifestHandler{ + ImageIndex: empty.Index, + } + + err = idx.Remove(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + it("should remove the image/index with the given digest", func() { + _, err := index.NewIndex("some/index", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + idx, err := layout.NewIndex("some/index", index.WithXDGRuntimePath(xdgPath), index.WithFormat(types.OCIImageIndex)) + h.AssertNil(t, err) + + ref, err := name.ParseReference( + "busybox:1.36-musl", + name.Insecure, + name.WeakValidation, + ) + h.AssertNil(t, err) + + err = idx.Add(ref, imgutil.WithAll(true)) + h.AssertNil(t, err) + + digest, err := name.NewDigest( + "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a", + name.WeakValidation, + name.Insecure, + ) + h.AssertNil(t, err) + + err = idx.Remove(digest) + h.AssertNil(t, err) + + _, err = idx.OS(digest) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digest.Identifier()).Error()) + }) + }) + when("#Delete", func() { + it("should delete the given index", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithXDGRuntimePath(xdgPath), + index.WithKeychain(authn.DefaultKeychain), + ) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should return an error if the index is already deleted", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithXDGRuntimePath(xdgPath), + index.WithKeychain(authn.DefaultKeychain), + ) + h.AssertNil(t, err) + + err = idx.Delete() + h.AssertEq(t, err.Error(), "stat xdgPath/busybox:1.36-musl: no such file or directory") + }) + }) + }) +} diff --git a/layout/layout_test.go b/layout/layout_test.go index 973cb9a4..ee0bd5d6 100644 --- a/layout/layout_test.go +++ b/layout/layout_test.go @@ -8,19 +8,15 @@ import ( "testing" "time" - "github.com/google/go-containerregistry/pkg/v1/types" - + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" "github.com/buildpacks/imgutil" - - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/buildpacks/imgutil/layout" - "github.com/sclevine/spec" - "github.com/sclevine/spec/report" - h "github.com/buildpacks/imgutil/testhelpers" ) @@ -95,7 +91,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { it("sets all platform required fields for windows", func() { img, err := layout.NewImage( imagePath, - layout.WithDefaultPlatform(imgutil.Platform{ + layout.WithDefaultPlatform(v1.Platform{ Architecture: "arm", OS: "windows", OSVersion: "10.0.17763.316", @@ -124,7 +120,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { it("sets all platform required fields for linux", func() { img, err := layout.NewImage( imagePath, - layout.WithDefaultPlatform(imgutil.Platform{ + layout.WithDefaultPlatform(v1.Platform{ Architecture: "arm", OS: "linux", }), @@ -1159,7 +1155,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) when("#Platform", func() { - var platform imgutil.Platform + var platform v1.Platform var image *layout.Image it.Before(func() { @@ -1167,7 +1163,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { image, err = layout.NewImage(imagePath) h.AssertNil(t, err) - platform = imgutil.Platform{ + platform = v1.Platform{ Architecture: "amd64", OS: "linux", OSVersion: "5678", @@ -1179,16 +1175,44 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) it("Platform values are saved on disk in OCI layout format", func() { - image.SetArchitecture("amd64") - image.SetOS("linux") - image.SetOSVersion("1234") + var ( + os = "linux" + arch = "amd64" + variant = "some-variant" + osVersion = "1234" + features = []string{"some-features"} + osFeatures = []string{"some-osFeatures"} + urls = []string{"some-urls"} + annos = map[string]string{"some-key": "some-value"} + ) + image.SetOS(os) + image.SetArchitecture(arch) + image.SetVariant(variant) + image.SetOSVersion(osVersion) + image.SetFeatures(features) + image.SetOSFeatures(osFeatures) + image.SetURLs(urls) + image.SetAnnotations(annos) image.Save() - _, configFile := h.ReadManifestAndConfigFile(t, imagePath) - h.AssertEq(t, configFile.OS, "linux") - h.AssertEq(t, configFile.Architecture, "amd64") - h.AssertEq(t, configFile.OSVersion, "1234") + mfest, configFile := h.ReadManifestAndConfigFile(t, imagePath) + h.AssertEq(t, configFile.OS, os) + h.AssertEq(t, configFile.Architecture, arch) + h.AssertEq(t, configFile.Variant, variant) + h.AssertEq(t, configFile.OSVersion, osVersion) + h.AssertEq(t, configFile.OSFeatures, osFeatures) + + h.AssertEq(t, mfest.Subject.Platform.OS, os) + h.AssertEq(t, mfest.Subject.Platform.Architecture, arch) + h.AssertEq(t, mfest.Subject.Platform.Variant, variant) + h.AssertEq(t, mfest.Subject.Platform.OSVersion, osVersion) + h.AssertEq(t, mfest.Subject.Platform.Features, features) + h.AssertEq(t, mfest.Subject.Platform.OSFeatures, osFeatures) + h.AssertEq(t, mfest.Subject.URLs, urls) + h.AssertEq(t, mfest.Subject.Annotations, annos) + + h.AssertEq(t, mfest.Annotations, annos) }) it("Default Platform values are saved on disk in OCI layout format", func() { diff --git a/layout/new.go b/layout/new.go index 91cc6dde..021b624b 100644 --- a/layout/new.go +++ b/layout/new.go @@ -2,12 +2,62 @@ package layout import ( "fmt" + "path/filepath" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/pkg/errors" "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" ) +// NewIndex will return a local OCI ImageIndex that can be modified and saved to a registry +func NewIndex(repoName string, ops ...index.Option) (idx imgutil.ImageIndex, err error) { + var idxOps = &index.Options{} + ops = append(ops, index.WithRepoName(repoName)) + + for _, op := range ops { + err = op(idxOps) + if err != nil { + return idx, err + } + } + + path, err := layout.FromPath(filepath.Join(idxOps.XDGRuntimePath(), idxOps.RepoName())) + if err != nil { + return idx, err + } + + imgIdx, err := path.ImageIndex() + if err != nil { + return idx, err + } + + mfest, err := imgIdx.IndexManifest() + if err != nil { + return idx, err + } + + if mfest == nil { + return idx, imgutil.ErrManifestUndefined + } + + if mfest.MediaType != types.OCIImageIndex { + return nil, errors.New("no oci image index found") + } + + idxOptions := imgutil.IndexOptions{ + KeyChain: idxOps.Keychain(), + XdgPath: idxOps.XDGRuntimePath(), + Reponame: idxOps.RepoName(), + InsecureRegistry: idxOps.Insecure(), + } + + return imgutil.NewManifestHandler(imgIdx, idxOptions), nil +} + func NewImage(path string, ops ...ImageOption) (*Image, error) { options := &imgutil.ImageOptions{} for _, op := range ops { @@ -58,12 +108,12 @@ func NewImage(path string, ops ...ImageOption) (*Image, error) { }, nil } -func processDefaultPlatformOption(requestedPlatform imgutil.Platform) imgutil.Platform { - var emptyPlatform imgutil.Platform - if requestedPlatform != emptyPlatform { +func processDefaultPlatformOption(requestedPlatform v1.Platform) v1.Platform { + var emptyPlatform v1.Platform + if emptyPlatform.Satisfies(requestedPlatform) { return requestedPlatform } - return imgutil.Platform{ + return v1.Platform{ OS: "linux", Architecture: "amd64", } @@ -72,7 +122,7 @@ func processDefaultPlatformOption(requestedPlatform imgutil.Platform) imgutil.Pl // newImageFromPath creates a layout image from the given path. // * If an image index for multiple platforms exists, it will try to select the image according to the platform provided. // * If the image does not exist, then nothing is returned. -func newImageFromPath(path string, withPlatform imgutil.Platform) (v1.Image, error) { +func newImageFromPath(path string, withPlatform v1.Platform) (v1.Image, error) { if !imageExists(path) { return nil, nil } @@ -94,7 +144,7 @@ func newImageFromPath(path string, withPlatform imgutil.Platform) (v1.Image, err // imageFromIndex creates a v1.Image from the given Image Index, selecting the image manifest // that matches the given OS and architecture. -func imageFromIndex(index v1.ImageIndex, platform imgutil.Platform) (v1.Image, error) { +func imageFromIndex(index v1.ImageIndex, platform v1.Platform) (v1.Image, error) { manifestList, err := index.IndexManifest() if err != nil { return nil, err diff --git a/layout/new_test.go b/layout/new_test.go new file mode 100644 index 00000000..94670aca --- /dev/null +++ b/layout/new_test.go @@ -0,0 +1,111 @@ +package layout_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" + "github.com/buildpacks/imgutil/layout" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestRemoteNew(t *testing.T) { + spec.Run(t, "RemoteNew", testRemoteNew, spec.Sequential(), spec.Report(report.Terminal{})) +} + +var ( + xdgPath = "xdgPath" + repoName = "some/index" + idx imgutil.ImageIndex + err error +) + +func testRemoteNew(t *testing.T, when spec.G, it spec.S) { + when("#NewIndex", func() { + it.Before(func() { + idx, err = index.NewIndex( + repoName, + index.WithFormat(types.OCIImageIndex), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + }) + it("should have expected indexOptions", func() { + idx, err = layout.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + h.AssertEq(t, imgIdx.Options.Reponame, repoName) + h.AssertEq(t, imgIdx.Options.XdgPath, xdgPath) + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should return an error when invalid repoName is passed", func() { + idx, err = layout.NewIndex( + repoName+"Image", + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNotEq(t, err, nil) + h.AssertNil(t, idx) + }) + it("should return ImageIndex with expected output", func() { + idx, err = layout.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, nil) + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should able to call #ImageIndex", func() { + idx, err = layout.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + hash, err := v1.NewHash("sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a") + h.AssertNil(t, err) + + _, err = imgIdx.ImageIndex.ImageIndex(hash) + h.AssertNotEq(t, err.Error(), "empty index") + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should able to call #Image", func() { + idx, err = layout.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + hash, err := v1.NewHash("sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a") + h.AssertNil(t, err) + + _, err = imgIdx.ImageIndex.Image(hash) + h.AssertNotEq(t, err.Error(), "empty index") + + err = idx.Delete() + h.AssertNil(t, err) + }) + }) +} diff --git a/layout/options.go b/layout/options.go index 1204943f..67aa6f39 100644 --- a/layout/options.go +++ b/layout/options.go @@ -43,7 +43,7 @@ func WithConfig(c *v1.Config) func(*imgutil.ImageOptions) { // WithDefaultPlatform provides the default Architecture/OS/OSVersion if no base image is provided, // or if the provided image inputs (base and previous) are manifest lists. -func WithDefaultPlatform(p imgutil.Platform) func(*imgutil.ImageOptions) { +func WithDefaultPlatform(p v1.Platform) func(*imgutil.ImageOptions) { return func(o *imgutil.ImageOptions) { o.Platform = p } diff --git a/layout/save.go b/layout/save.go index ce948a3f..35ce8fa5 100644 --- a/layout/save.go +++ b/layout/save.go @@ -1,6 +1,7 @@ package layout import ( + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/buildpacks/imgutil" @@ -27,6 +28,52 @@ func (i *Image) SaveAs(name string, additionalNames ...string) error { ops = append(ops, WithoutLayers()) } + i.Image, err = imgutil.MutateManifest(i, func(mfest *v1.Manifest) { + config := mfest.Config + if annos, _ := i.Annotations(); len(annos) != 0 { + mfest.Annotations = annos + config.Annotations = annos + + } + + if urls, _ := i.URLs(); len(urls) != 0 { + config.URLs = append(config.URLs, urls...) + } + + if config.Platform == nil { + config.Platform = &v1.Platform{} + } + + if features, _ := i.Features(); len(features) != 0 { + config.Platform.Features = append(config.Platform.Features, features...) + } + + if osFeatures, _ := i.OSFeatures(); len(osFeatures) != 0 { + config.Platform.OSFeatures = append(config.Platform.OSFeatures, osFeatures...) + } + + if os, _ := i.OS(); os != "" { + config.Platform.OS = os + } + + if arch, _ := i.Architecture(); arch != "" { + config.Platform.Architecture = arch + } + + if variant, _ := i.Variant(); variant != "" { + config.Platform.Variant = variant + } + + if osVersion, _ := i.OSVersion(); osVersion != "" { + config.Platform.OSVersion = osVersion + } + + mfest.Config = config + }) + if err != nil { + return err + } + var ( pathsToSave = append([]string{name}, additionalNames...) diagnostics []imgutil.SaveDiagnostic diff --git a/local/index.go b/local/index.go deleted file mode 100644 index 02ef16ce..00000000 --- a/local/index.go +++ /dev/null @@ -1,236 +0,0 @@ -package local - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/match" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" -) - -type ImageIndex struct { - repoName string - path string - index v1.ImageIndex -} - -// Add appends a new image manifest to the local ImageIndex/ManifestList. -// We have not implemented nested indexes yet. -// See specification for more info: -// https://github.com/opencontainers/image-spec/blob/0b40f0f367c396cc5a7d6a2e8c8842271d3d3844/image-index.md#image-index-property-descriptions -func (i *ImageIndex) Add(repoName string) error { - ref, err := name.ParseReference(repoName) - if err != nil { - return err - } - - desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return err - } - - img, err := desc.Image() - if err != nil { - return err - } - - cfg, err := img.ConfigFile() - - if err != nil { - return errors.Wrapf(err, "getting config file for image %q", repoName) - } - if cfg == nil { - return fmt.Errorf("missing config for image %q", repoName) - } - - platform := v1.Platform{} - platform.Architecture = cfg.Architecture - platform.OS = cfg.OS - - desc.Descriptor.Platform = &platform - - indexRef, err := name.ParseReference(i.repoName) - if err != nil { - return err - } - - // Check if the image is in the same repository as the index - // If it is in a different repository then copy the image to - // the same repository as the index - if ref.Context().Name() != indexRef.Context().Name() { - imgRefName := indexRef.Context().Name() + "@" + desc.Digest.Algorithm + ":" + desc.Digest.Hex - imgRef, err := name.ParseReference(imgRefName) - if err != nil { - return err - } - - err = remote.Write(imgRef, img, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return errors.Wrapf(err, "failed to copy image '%s' to index repository", imgRef.Name()) - } - } - - i.index = mutate.AppendManifests(i.index, mutate.IndexAddendum{Add: img, Descriptor: desc.Descriptor}) - - return nil -} - -// Remove method removes the specified manifest from the local index -func (i *ImageIndex) Remove(repoName string) error { - ref, err := name.ParseReference(repoName) - if err != nil { - return err - } - - desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return err - } - - i.index = mutate.RemoveManifests(i.index, match.Digests(desc.Digest)) - - return nil -} - -// Delete method removes the specified index from the local storage -func (i *ImageIndex) Delete(additionalNames ...string) error { - _, err := name.ParseReference(i.repoName) - if err != nil { - return err - } - - manifestPath := filepath.Join(i.path, makeFileSafeName(i.repoName)) - err = os.Remove(manifestPath) - if err != nil { - return err - } - - return nil -} - -// Save stores the ImageIndex manifest information in a plain text in the ined file in JSON format. -func (i *ImageIndex) Save(additionalNames ...string) error { - indexManifest, err := i.index.IndexManifest() - if err != nil { - return err - } - - rawManifest, err := json.MarshalIndent(indexManifest, "", " ") - if err != nil { - return err - } - - manifestDir := filepath.Join(i.path, makeFileSafeName(i.repoName)) - - err = os.WriteFile(manifestDir, rawManifest, os.ModePerm) - if err != nil { - return err - } - - return nil -} - -// Change a reference name string into a valid file name -// Ex: cnbs/sample-package:hello-multiarch-universe -// to cnbs_sample-package-hello-multiarch-universe -func makeFileSafeName(ref string) string { - fileName := strings.ReplaceAll(ref, ":", "-") - return strings.ReplaceAll(fileName, "/", "_") -} - -func (i *ImageIndex) Name() string { - return i.repoName -} - -// Fields which are allowed to be annotated in a local index -type AnnotateFields struct { - Architecture string - OS string - Variant string -} - -// AnnotateManifest changes the fields of the local index which -// are not empty string in the provided AnnotateField structure. -func (i *ImageIndex) AnnotateManifest(manifestName string, opts AnnotateFields) error { - path := filepath.Join(i.path, makeFileSafeName(i.repoName)) - - manifest, err := i.index.IndexManifest() - if err != nil { - return err - } - - ref, err := name.ParseReference(manifestName) - if err != nil { - return err - } - - desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return err - } - - for i, iDesc := range manifest.Manifests { - if iDesc.Digest.String() == desc.Digest.String() { - if opts.Architecture != "" { - manifest.Manifests[i].Platform.Architecture = opts.Architecture - } - - if opts.OS != "" { - manifest.Manifests[i].Platform.OS = opts.OS - } - - if opts.Variant != "" { - manifest.Manifests[i].Platform.Variant = opts.Variant - } - - data, err := json.Marshal(manifest) - if err != nil { - return err - } - - err = os.WriteFile(path, data, os.ModePerm) - if err != nil { - return err - } - - return nil - } - } - - return errors.Errorf("Manifest %s not found", manifestName) -} - -// GetIndexManifest will look for a file the given index in the specified path and -// if found it will return a v1.IndexManifest. -// It is assumed that the local index file name is derived using makeFileSafeName() -func GetIndexManifest(repoName string, path string) (v1.IndexManifest, error) { - var manifest v1.IndexManifest - - _, err := name.ParseReference(repoName) - if err != nil { - return manifest, err - } - - manifestDir := filepath.Join(path, makeFileSafeName(repoName)) - - jsonFile, err := os.ReadFile(manifestDir) - if err != nil { - return manifest, errors.Wrapf(err, "Reading local index %q in path %q", repoName, path) - } - - err = json.Unmarshal(jsonFile, &manifest) - if err != nil { - return manifest, errors.Wrapf(err, "Decoding local index %q", repoName) - } - - return manifest, nil -} diff --git a/local/index_options.go b/local/index_options.go deleted file mode 100644 index 226d080f..00000000 --- a/local/index_options.go +++ /dev/null @@ -1,30 +0,0 @@ -package local - -import ( - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/buildpacks/imgutil" -) - -type ImageIndexOption func(*indexOptions) error - -type indexOptions struct { - mediaTypes imgutil.MediaTypes - manifest v1.IndexManifest -} - -// WithIndexMediaTypes lets a caller set the desired media types for the index manifest -func WithIndexMediaTypes(requested imgutil.MediaTypes) ImageIndexOption { - return func(opts *indexOptions) error { - opts.mediaTypes = requested - return nil - } -} - -// WithManifest uses an existing v1.IndexManifest as a base to create the index -func WithManifest(manifest v1.IndexManifest) ImageIndexOption { - return func(opts *indexOptions) error { - opts.manifest = manifest - return nil - } -} diff --git a/local/local.go b/local/local.go index 1a5edcec..18ddf8c1 100644 --- a/local/local.go +++ b/local/local.go @@ -17,6 +17,8 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" + ggcrTypes "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/buildpacks/imgutil" ) @@ -148,6 +150,22 @@ func (i *Image) OSVersion() (string, error) { return i.inspect.OsVersion, nil } +func (i *Image) Features() ([]string, error) { + return nil, errors.New("not yet implemented") +} + +func (i *Image) OSFeatures() ([]string, error) { + return nil, errors.New("not yet implemented") +} + +func (i *Image) URLs() ([]string, error) { + return nil, errors.New("not yet implemented") +} + +func (i *Image) Annotations() (map[string]string, error) { + return nil, errors.New("not yet implemented") +} + func (i *Image) TopLayer() (string, error) { all := i.inspect.RootFS.Layers @@ -196,6 +214,14 @@ func (i *Image) SetEntrypoint(ep ...string) error { return nil } +func (i *Image) Digest() (v1.Hash, error) { + return v1.NewHash(i.inspect.ID) +} + +func (i *Image) MediaType() (ggcrTypes.MediaType, error) { + return ggcrTypes.DockerManifestSchema2, nil +} + func (i *Image) SetEnv(key, val string) error { ignoreCase := i.inspect.Os == "windows" for idx, kv := range i.inspect.Config.Env { @@ -246,6 +272,22 @@ func (i *Image) SetVariant(v string) error { return nil } +func (i *Image) SetFeatures(features []string) error { + return errors.New("not yet implemented") +} + +func (i *Image) SetOSFeatures(osFeatures []string) error { + return errors.New("not yet implemented") +} + +func (i *Image) SetURLs(urls []string) error { + return errors.New("not yet implemented") +} + +func (i *Image) SetAnnotations(annotations map[string]string) error { + return errors.New("not yet implemented") +} + func (i *Image) SetWorkingDir(dir string) error { i.inspect.Config.WorkingDir = dir return nil diff --git a/local/local_test.go b/local/local_test.go index e8d4905c..34c6950d 100644 --- a/local/local_test.go +++ b/local/local_test.go @@ -70,7 +70,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { it("sets sensible defaults from daemon for all required fields", func() { // os, architecture, and rootfs are required per https://github.com/opencontainers/image-spec/blob/master/config.md - img, err := local.NewImage(newTestImageName(), dockerClient) + img, err := local.NewImage(newTestImageName(), dockerClient, local.WithDefaultPlatform(v1.Platform{OS: "linux", Architecture: "amd64"})) h.AssertNil(t, err) h.AssertNil(t, img.Save()) @@ -103,7 +103,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { img, err := local.NewImage( newTestImageName(), dockerClient, - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: expectedArmArch, OS: daemonOS, OSVersion: expectedOSVersion, @@ -273,7 +273,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { newTestImageName(), dockerClient, local.FromBaseImage(armBaseImageName), - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: "not-an-arch", OSVersion: "10.0.99999.9999", }), @@ -298,7 +298,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { newTestImageName(), dockerClient, local.FromBaseImage("some-bad-repo-name"), - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: "arm64", OS: daemonOS, OSVersion: "10.0.99999.9999", @@ -372,7 +372,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { newTestImageName(), dockerClient, local.WithPreviousImage(armBaseImageName), - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: "some-fake-os", }), ) diff --git a/local/new.go b/local/new.go index 29e79880..11f58c85 100644 --- a/local/new.go +++ b/local/new.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "path/filepath" "sync" "time" @@ -15,12 +16,60 @@ import ( "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + ggcrTypes "github.com/google/go-containerregistry/pkg/v1/types" "github.com/pkg/errors" "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" "github.com/buildpacks/imgutil/layer" ) +// NewIndex will return a new local Docker ImageIndex that can be modified and saved to a registry +func NewIndex(repoName string, ops ...index.Option) (idx imgutil.ImageIndex, err error) { + var idxOps = &index.Options{} + ops = append(ops, index.WithRepoName(repoName)) + + for _, op := range ops { + err = op(idxOps) + if err != nil { + return idx, err + } + } + + path, err := layout.FromPath(filepath.Join(idxOps.XDGRuntimePath(), idxOps.RepoName())) + if err != nil { + return idx, err + } + + imgIdx, err := path.ImageIndex() + if err != nil { + return idx, err + } + + mfest, err := imgIdx.IndexManifest() + if err != nil { + return idx, err + } + + if mfest == nil { + return idx, imgutil.ErrManifestUndefined + } + + if mfest.MediaType != ggcrTypes.DockerManifestList { + return nil, errors.New("no docker image index found") + } + + idxOptions := imgutil.IndexOptions{ + KeyChain: idxOps.Keychain(), + XdgPath: idxOps.XDGRuntimePath(), + Reponame: idxOps.RepoName(), + InsecureRegistry: idxOps.Insecure(), + } + + return imgutil.NewManifestHandler(imgIdx, idxOptions), nil +} + // NewImage returns a new Image that can be modified and saved to a registry. func NewImage(repoName string, dockerClient DockerClient, ops ...ImageOption) (*Image, error) { imageOpts := &options{} @@ -35,7 +84,7 @@ func NewImage(repoName string, dockerClient DockerClient, ops ...ImageOption) (* return nil, err } - if (imageOpts.platform != imgutil.Platform{}) { + if !platform.Satisfies(imageOpts.platform) { if err := validatePlatformOption(platform, imageOpts.platform); err != nil { return nil, err } @@ -85,19 +134,19 @@ func NewImage(repoName string, dockerClient DockerClient, ops ...ImageOption) (* return image, nil } -func defaultPlatform(dockerClient DockerClient) (imgutil.Platform, error) { +func defaultPlatform(dockerClient DockerClient) (v1.Platform, error) { versionInfo, err := dockerClient.ServerVersion(context.Background()) if err != nil { - return imgutil.Platform{}, err + return v1.Platform{}, err } - return imgutil.Platform{ + return v1.Platform{ OS: versionInfo.Os, Architecture: versionInfo.Arch, }, nil } -func validatePlatformOption(defaultPlatform imgutil.Platform, optionPlatform imgutil.Platform) error { +func validatePlatformOption(defaultPlatform v1.Platform, optionPlatform v1.Platform) error { if optionPlatform.OS != "" && optionPlatform.OS != defaultPlatform.OS { return fmt.Errorf("invalid os: platform os %q must match the daemon os %q", optionPlatform.OS, defaultPlatform.OS) } @@ -105,16 +154,17 @@ func validatePlatformOption(defaultPlatform imgutil.Platform, optionPlatform img return nil } -func defaultInspect(platform imgutil.Platform) types.ImageInspect { +func defaultInspect(platform v1.Platform) types.ImageInspect { return types.ImageInspect{ Os: platform.OS, Architecture: platform.Architecture, OsVersion: platform.OSVersion, + Variant: platform.Variant, Config: &container.Config{}, } } -func processPreviousImageOption(image *Image, prevImageRepoName string, platform imgutil.Platform, dockerClient DockerClient) error { +func processPreviousImageOption(image *Image, prevImageRepoName string, platform v1.Platform, dockerClient DockerClient) error { inspect, err := inspectOptionalImage(dockerClient, prevImageRepoName, platform) if err != nil { return err @@ -141,7 +191,7 @@ func processPreviousImageOption(image *Image, prevImageRepoName string, platform return nil } -func inspectOptionalImage(docker DockerClient, imageName string, platform imgutil.Platform) (types.ImageInspect, error) { +func inspectOptionalImage(docker DockerClient, imageName string, platform v1.Platform) (types.ImageInspect, error) { var ( err error inspect types.ImageInspect @@ -170,7 +220,7 @@ func historyOptionalImage(docker DockerClient, imageName string) ([]image.Histor return history, nil } -func processBaseImageOption(image *Image, baseImageRepoName string, platform imgutil.Platform, dockerClient DockerClient) error { +func processBaseImageOption(image *Image, baseImageRepoName string, platform v1.Platform, dockerClient DockerClient) error { inspect, err := inspectOptionalImage(dockerClient, baseImageRepoName, platform) if err != nil { return err diff --git a/local/new_index.go b/local/new_index.go deleted file mode 100644 index 30081234..00000000 --- a/local/new_index.go +++ /dev/null @@ -1,110 +0,0 @@ -package local - -import ( - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/types" - - "github.com/buildpacks/imgutil" -) - -func NewIndex(repoName string, path string, ops ...ImageIndexOption) (*ImageIndex, error) { - ref, err := name.ParseReference(repoName, name.WeakValidation) - if err != nil { - return nil, err - } - - indexOpts := &indexOptions{} - for _, op := range ops { - if err := op(indexOpts); err != nil { - return nil, err - } - } - - // If WithManifest option is given, create an index using - // the provided v1.IndexManifest - if len(indexOpts.manifest.Manifests) != 0 { - index, err := emptyIndex(indexOpts.manifest.MediaType) - if err != nil { - return nil, err - } - - for _, manifest := range indexOpts.manifest.Manifests { - img, _ := emptyImage(imgutil.Platform{ - Architecture: manifest.Platform.Architecture, - OS: manifest.Platform.OS, - OSVersion: manifest.Platform.OSVersion, - }) - index = mutate.AppendManifests(index, mutate.IndexAddendum{Add: img, Descriptor: manifest}) - } - - idx := &ImageIndex{ - repoName: repoName, - path: path, - index: index, - } - - return idx, nil - } - - // If index already exists in registry, use it as a base - desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err == nil { - index, err := desc.ImageIndex() - if err != nil { - return nil, err - } - - idx := &ImageIndex{ - repoName: repoName, - path: path, - index: index, - } - - return idx, nil - } - - mediaType := defaultMediaType() - if indexOpts.mediaTypes.IndexManifestType() != "" { - mediaType = indexOpts.mediaTypes - } - - index, err := emptyIndex(mediaType.IndexManifestType()) - if err != nil { - return nil, err - } - - ridx := &ImageIndex{ - repoName: repoName, - path: path, - index: index, - } - - return ridx, nil -} - -func emptyIndex(mediaType types.MediaType) (v1.ImageIndex, error) { - return mutate.IndexMediaType(empty.Index, mediaType), nil -} - -func emptyImage(platform imgutil.Platform) (v1.Image, error) { - cfg := &v1.ConfigFile{ - Architecture: platform.Architecture, - OS: platform.OS, - OSVersion: platform.OSVersion, - RootFS: v1.RootFS{ - Type: "layers", - DiffIDs: []v1.Hash{}, - }, - } - - return mutate.ConfigFile(empty.Image, cfg) -} - -func defaultMediaType() imgutil.MediaTypes { - return imgutil.DockerTypes -} diff --git a/local/new_test.go b/local/new_test.go new file mode 100644 index 00000000..ef933f0f --- /dev/null +++ b/local/new_test.go @@ -0,0 +1,107 @@ +package local_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" + "github.com/buildpacks/imgutil/local" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestRemoteNew(t *testing.T) { + spec.Run(t, "RemoteNew", testRemoteNew, spec.Sequential(), spec.Report(report.Terminal{})) +} + +var ( + xdgPath = "xdgPath" + repoName = "some/index" +) + +func testRemoteNew(t *testing.T, when spec.G, it spec.S) { + var ( + idx imgutil.ImageIndex + err error + ) + when("#NewIndex", func() { + it.Before(func() { + idx, err = index.NewIndex( + repoName, + index.WithFormat(types.DockerManifestList), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + }) + it("should have expected indexOptions", func() { + idx, err = local.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + h.AssertEq(t, imgIdx.Options.Reponame, repoName) + h.AssertEq(t, imgIdx.Options.XdgPath, xdgPath) + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should return an error when invalid repoName is passed", func() { + idx, err = local.NewIndex( + repoName+"Image", + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNotEq(t, err, nil) + h.AssertNil(t, idx) + }) + it("should return ImageIndex with expected output", func() { + idx, err = local.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + h.AssertNotEq(t, idx, nil) + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should able to call #ImageIndex", func() { + idx, err = local.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + _, err = imgIdx.ImageIndex.ImageIndex(v1.Hash{}) + h.AssertNotEq(t, err.Error(), "empty index") + + err = idx.Delete() + h.AssertNil(t, err) + }) + it("should able to call #Image", func() { + idx, err = local.NewIndex( + repoName, + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + _, err = imgIdx.Image(v1.Hash{}) + h.AssertNotEq(t, err.Error(), "empty index") + + err = idx.Delete() + h.AssertNil(t, err) + }) + }) +} diff --git a/local/options.go b/local/options.go index c8563d45..318a22d2 100644 --- a/local/options.go +++ b/local/options.go @@ -4,14 +4,13 @@ import ( "time" "github.com/docker/docker/api/types/container" - - "github.com/buildpacks/imgutil" + v1 "github.com/google/go-containerregistry/pkg/v1" ) type ImageOption func(*options) error type options struct { - platform imgutil.Platform + platform v1.Platform baseImageRepoName string prevImageRepoName string withHistory bool @@ -46,7 +45,7 @@ func WithConfig(config *container.Config) ImageOption { // WithDefaultPlatform provides Architecture/OS/OSVersion defaults for the new image. // Defaults for a new image are ignored when FromBaseImage returns an image. -func WithDefaultPlatform(platform imgutil.Platform) ImageOption { +func WithDefaultPlatform(platform v1.Platform) ImageOption { return func(i *options) error { i.platform = platform return nil diff --git a/locallayout/image_test.go b/locallayout/image_test.go index 61f0d848..9b234a91 100644 --- a/locallayout/image_test.go +++ b/locallayout/image_test.go @@ -100,7 +100,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { img, err := local.NewImage( newTestImageName(), dockerClient, - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: expectedArmArch, OS: daemonOS, OSVersion: expectedOSVersion, @@ -270,7 +270,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { newTestImageName(), dockerClient, local.FromBaseImage(armBaseImageName), - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: "not-an-arch", OSVersion: "10.0.99999.9999", }), @@ -295,7 +295,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { newTestImageName(), dockerClient, local.FromBaseImage("some-bad-repo-name"), - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: "arm64", OS: daemonOS, OSVersion: "10.0.99999.9999", @@ -369,7 +369,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { newTestImageName(), dockerClient, local.WithPreviousImage(armBaseImageName), - local.WithDefaultPlatform(imgutil.Platform{ + local.WithDefaultPlatform(v1.Platform{ Architecture: "some-fake-os", }), ) diff --git a/locallayout/new.go b/locallayout/new.go index 1ca41033..d69f6f67 100644 --- a/locallayout/new.go +++ b/locallayout/new.go @@ -65,29 +65,30 @@ func NewImage(repoName string, dockerClient DockerClient, ops ...func(*imgutil.I }, nil } -func processDefaultPlatformOption(requestedPlatform imgutil.Platform, dockerClient DockerClient) (imgutil.Platform, error) { +func processDefaultPlatformOption(requestedPlatform v1.Platform, dockerClient DockerClient) (v1.Platform, error) { dockerPlatform, err := defaultPlatform(dockerClient) if err != nil { - return imgutil.Platform{}, err + return v1.Platform{}, err } - if (requestedPlatform == imgutil.Platform{}) { + if dockerPlatform.Satisfies(requestedPlatform) { return dockerPlatform, nil } if requestedPlatform.OS != "" && requestedPlatform.OS != dockerPlatform.OS { - return imgutil.Platform{}, + return v1.Platform{}, fmt.Errorf("invalid os: platform os %q must match the daemon os %q", requestedPlatform.OS, dockerPlatform.OS) } return requestedPlatform, nil } -func defaultPlatform(dockerClient DockerClient) (imgutil.Platform, error) { +func defaultPlatform(dockerClient DockerClient) (v1.Platform, error) { daemonInfo, err := dockerClient.ServerVersion(context.Background()) if err != nil { - return imgutil.Platform{}, err + return v1.Platform{}, err } - return imgutil.Platform{ + return v1.Platform{ OS: daemonInfo.Os, Architecture: daemonInfo.Arch, + OSVersion: daemonInfo.Version, }, nil } diff --git a/locallayout/options.go b/locallayout/options.go index 5a54dfc7..973f1dd8 100644 --- a/locallayout/options.go +++ b/locallayout/options.go @@ -26,7 +26,7 @@ func WithCreatedAt(t time.Time) func(*imgutil.ImageOptions) { } } -func WithDefaultPlatform(p imgutil.Platform) func(*imgutil.ImageOptions) { +func WithDefaultPlatform(p v1.Platform) func(*imgutil.ImageOptions) { return func(o *imgutil.ImageOptions) { o.Platform = p } diff --git a/locallayout/store.go b/locallayout/store.go index 08e98ed0..26c2b183 100644 --- a/locallayout/store.go +++ b/locallayout/store.go @@ -311,6 +311,51 @@ func (s *Store) SaveFile(image *Image, withName string) (string, error) { return "", err } + image.Image, err = imgutil.MutateManifest(*image, func(mfest *v1.Manifest) { + config := mfest.Config + if annos, _ := image.Annotations(); len(annos) != 0 { + mfest.Annotations = annos + config.Annotations = annos + } + + if urls, _ := image.URLs(); len(urls) != 0 { + config.URLs = append(config.URLs, urls...) + } + + if config.Platform == nil { + config.Platform = &v1.Platform{} + } + + if features, _ := image.Features(); len(features) != 0 { + config.Platform.Features = append(config.Platform.Features, features...) + } + + if osFeatures, _ := image.OSFeatures(); len(osFeatures) != 0 { + config.Platform.OSFeatures = append(config.Platform.OSFeatures, osFeatures...) + } + + if os, _ := image.OS(); os != "" { + config.Platform.OS = os + } + + if arch, _ := image.Architecture(); arch != "" { + config.Platform.Architecture = arch + } + + if variant, _ := image.Variant(); variant != "" { + config.Platform.Variant = variant + } + + if osVersion, _ := image.OSVersion(); osVersion != "" { + config.Platform.OSVersion = osVersion + } + + mfest.Config = config + }) + if err != nil { + return "", err + } + errs, _ := errgroup.WithContext(context.Background()) pr, pw := io.Pipe() diff --git a/locallayout/v1_facade.go b/locallayout/v1_facade.go index 35418e29..f421e9e7 100644 --- a/locallayout/v1_facade.go +++ b/locallayout/v1_facade.go @@ -64,9 +64,6 @@ func imageFrom(layers []v1.Layer, configFile *v1.ConfigFile, requestedTypes imgu retImage = mutate.ConfigMediaType(retImage, configType) // (3) set layers with the right media type additions := layersAddendum(layers, beforeHistory, requestedTypes.LayerType()) - if err != nil { - return nil, err - } retImage, err = mutate.Append(retImage, additions...) if err != nil { return nil, err diff --git a/new.go b/new.go index 5dfae9eb..e262c595 100644 --- a/new.go +++ b/new.go @@ -113,7 +113,7 @@ func (t MediaTypes) LayerType() types.MediaType { } } -func emptyV1(withPlatform Platform, withMediaTypes MediaTypes) (v1.Image, error) { +func emptyV1(withPlatform v1.Platform, withMediaTypes MediaTypes) (v1.Image, error) { configFile := &v1.ConfigFile{ Architecture: withPlatform.Architecture, History: []v1.History{}, @@ -194,9 +194,6 @@ func EnsureMediaTypesAndLayers(image v1.Image, requestedTypes MediaTypes, mutate // (4) set layers with the right media type additions := layersAddendum(layersToAdd, beforeHistory, requestedTypes.LayerType()) - if err != nil { - return nil, false, err - } retImage, err = mutate.Append(retImage, additions...) if err != nil { return nil, false, fmt.Errorf("failed to append layers: %w", err) @@ -291,3 +288,34 @@ func prepareNewWindowsImageIfNeeded(image *CNBImageCore) error { } return nil } + +func NewManifestHandler(ii v1.ImageIndex, ops IndexOptions) *ManifestHandler { + return &ManifestHandler{ + ImageIndex: ii, + Options: ops, + Annotate: NewAnnotate(), + RemovedManifests: make([]v1.Hash, 0), + Images: make(map[v1.Hash]v1.Descriptor), + } +} + +func NewAnnotate() Annotate { + return Annotate{ + Instance: make(map[v1.Hash]v1.Descriptor), + } +} + +func NewEmptyDockerIndex() v1.ImageIndex { + idx := empty.Index + return mutate.IndexMediaType(idx, types.DockerManifestList) +} + +func NewStringSet() *StringSet { + return &StringSet{items: make(map[string]bool)} +} + +func NewTaggableIndex(mfest *v1.IndexManifest) *TaggableIndex { + return &TaggableIndex{ + IndexManifest: mfest, + } +} diff --git a/new_test.go b/new_test.go new file mode 100644 index 00000000..7278040d --- /dev/null +++ b/new_test.go @@ -0,0 +1,67 @@ +package imgutil_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestNewIndex(t *testing.T) { + spec.Run(t, "IndexOptions", testNewIndex, spec.Sequential(), spec.Report(report.Terminal{})) +} + +func testNewIndex(t *testing.T, when spec.G, it spec.S) { + when("#NewManifestHandler", func() { + it("should create with expected Index", func() { + ih := imgutil.NewManifestHandler(empty.Index, imgutil.IndexOptions{}) + h.AssertEq(t, ih.ImageIndex, empty.Index) + }) + it("should create with expected Options", func() { + ops := imgutil.IndexOptions{ + XdgPath: "xdgPath", + Reponame: "some/repo", + InsecureRegistry: false, + } + + ih := imgutil.NewManifestHandler(empty.Index, ops) + h.AssertEq(t, ih.Options.InsecureRegistry, ops.InsecureRegistry) + h.AssertEq(t, ih.Options.Reponame, ops.Reponame) + h.AssertEq(t, ih.Options.XdgPath, ops.XdgPath) + h.AssertEq(t, ih.Options.KeyChain, ops.KeyChain) + }) + it("should create ManifestHandlers with not Nil maps and slices", func() { + ih := imgutil.NewManifestHandler(empty.Index, imgutil.IndexOptions{}) + h.AssertEq(t, len(ih.Annotate.Instance), 0) + h.AssertEq(t, len(ih.RemovedManifests), 0) + h.AssertEq(t, len(ih.Images), 0) + }) + }) + when("#NewEmptyDockerIndex", func() { + it("should return an empty docker index", func() { + idx := imgutil.NewEmptyDockerIndex() + h.AssertNotNil(t, idx) + + digest, err := idx.Digest() + h.AssertNil(t, err) + h.AssertNotEq(t, digest, v1.Hash{}) + + format, err := idx.MediaType() + h.AssertNil(t, err) + h.AssertEq(t, format, types.DockerManifestList) + }) + }) + when("#NewStringSet", func() { + it("should return not nil StringSet instance", func() { + stringSet := imgutil.NewStringSet() + h.AssertNotNil(t, stringSet) + h.AssertEq(t, stringSet.StringSlice(), []string(nil)) + }) + }) +} diff --git a/options.go b/options.go index 061e33a3..61c6c953 100644 --- a/options.go +++ b/options.go @@ -1,9 +1,13 @@ package imgutil import ( + "crypto/tls" + "net/http" "time" + "github.com/google/go-containerregistry/pkg/authn" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" ) type ImageOptions struct { @@ -12,7 +16,7 @@ type ImageOptions struct { Config *v1.Config CreatedAt time.Time MediaTypes MediaTypes - Platform Platform + Platform v1.Platform PreserveDigest bool PreserveHistory bool WithoutLayers bool // only relevant for layout images @@ -21,3 +25,157 @@ type ImageOptions struct { BaseImage v1.Image PreviousImage v1.Image } + +type IndexAddOption func(*AddOptions) +type IndexPushOption func(*PushOptions) error + +type AddOptions struct { + All bool + OS, Arch, Variant, OSVersion string + Features, OSFeatures []string + Annotations map[string]string + Local bool + Image EditableImage +} + +type PushOptions struct { + Insecure, Purge bool + // The Format the Index should be. One of Docker or OCI + Format types.MediaType + // Tags with which the index should be pushed to registry + Tags []string +} + +type IndexOptions struct { + KeyChain authn.Keychain + XdgPath, Reponame string + InsecureRegistry bool +} + +func (o *IndexOptions) Keychain() authn.Keychain { + return o.KeyChain +} + +func (o *IndexOptions) XDGRuntimePath() string { + return o.XdgPath +} + +func (o *IndexOptions) RepoName() string { + return o.Reponame +} + +func (o *IndexOptions) Insecure() bool { + return o.InsecureRegistry +} + +// Add all images within the index +func WithAll(all bool) IndexAddOption { + return func(a *AddOptions) { + a.All = all + } +} + +// Add a single image from index with given OS +func WithOS(os string) IndexAddOption { + return func(a *AddOptions) { + a.OS = os + } +} + +// Add a Local image to Index +func WithLocalImage(image EditableImage) IndexAddOption { + return func(a *AddOptions) { + a.Local = true + a.Image = image + } +} + +// Add a single image from index with given Architecture +func WithArchitecture(arch string) IndexAddOption { + return func(a *AddOptions) { + a.Arch = arch + } +} + +// Add a single image from index with given Variant +func WithVariant(variant string) IndexAddOption { + return func(a *AddOptions) { + a.Variant = variant + } +} + +// Add a single image from index with given OSVersion +func WithOSVersion(osVersion string) IndexAddOption { + return func(a *AddOptions) { + a.OSVersion = osVersion + } +} + +// Add a single image from index with given Features +func WithFeatures(features []string) IndexAddOption { + return func(a *AddOptions) { + a.Features = features + } +} + +// Add a single image from index with given OSFeatures +func WithOSFeatures(osFeatures []string) IndexAddOption { + return func(a *AddOptions) { + a.OSFeatures = osFeatures + } +} + +// Add a single image from index with given Annotations +func WithAnnotations(annotations map[string]string) IndexAddOption { + return func(a *AddOptions) { + a.Annotations = annotations + } +} + +// Push index to Insecure Registry +func WithInsecure(insecure bool) IndexPushOption { + return func(a *PushOptions) error { + a.Insecure = insecure + return nil + } +} + +// If true, Deletes index from local filesystem after pushing to registry +func WithPurge(purge bool) IndexPushOption { + return func(a *PushOptions) error { + a.Purge = purge + return nil + } +} + +// Push the Index with given format +func WithFormat(format types.MediaType) IndexPushOption { + return func(a *PushOptions) error { + if !format.IsIndex() { + return ErrUnknownMediaType(format) + } + a.Format = format + return nil + } +} + +// Push the Index with given format +func WithTags(tags ...string) IndexPushOption { + return func(a *PushOptions) error { + a.Tags = tags + return nil + } +} + +func GetTransport(insecure bool) http.RoundTripper { + // #nosec G402 + if insecure { + return &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + } + + return http.DefaultTransport +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 00000000..612abf60 --- /dev/null +++ b/options_test.go @@ -0,0 +1,120 @@ +package imgutil_test + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestIndexOptions(t *testing.T) { + spec.Run(t, "IndexOptions", testIndexOptions, spec.Sequential(), spec.Report(report.Terminal{})) +} + +var ( + indexOptions = imgutil.IndexOptions{ + XdgPath: "/xdgPath", + Reponame: "some/repoName", + InsecureRegistry: true, + } + addOptions = &imgutil.AddOptions{} + pushOptions = &imgutil.PushOptions{} +) + +func testIndexOptions(t *testing.T, when spec.G, it spec.S) { + when("#IndexOption", func() { + it("#XDGRuntimePath should return expected XDGRuntimePath", func() { + h.AssertEq(t, indexOptions.XDGRuntimePath(), "/xdgPath") + }) + it("#RepoName should return expected RepoName", func() { + h.AssertEq(t, indexOptions.RepoName(), "some/repoName") + }) + it("#Insecure should return expected boolean", func() { + h.AssertEq(t, indexOptions.Insecure(), true) + }) + it("#Keychain should return expected Keychain", func() { + h.AssertEq(t, indexOptions.Keychain(), nil) + }) + }) + when("#AddOptions", func() { + it.Before(func() { + addOptions = &imgutil.AddOptions{} + }) + it("#WithAll", func() { + op := imgutil.WithAll(true) + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + it("#WithOS", func() { + op := imgutil.WithOS("some-os") + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + it("#WithArchitecture", func() { + op := imgutil.WithArchitecture("some-arch") + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + it("#WithVariant", func() { + op := imgutil.WithVariant("some-variant") + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + it("#WithOSVersion", func() { + op := imgutil.WithOSVersion("some-osVersion") + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + it("#WithFeatures", func() { + op := imgutil.WithFeatures([]string{"some-features"}) + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + it("#WithOSFeatures", func() { + op := imgutil.WithOSFeatures([]string{"some-osFeatures"}) + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + it("#WithAnnotations", func() { + op := imgutil.WithAnnotations(map[string]string{"some-key": "some-value"}) + op(addOptions) + h.AssertNotEq(t, addOptions, imgutil.AddOptions{}) + }) + }) + when("#PushOptions", func() { + it.Before(func() { + pushOptions = &imgutil.PushOptions{} + }) + it("#WithInsecure", func() { + op := imgutil.WithInsecure(true) + h.AssertNil(t, op(pushOptions)) + h.AssertEq(t, pushOptions.Insecure, true) + }) + it("#WithPurge", func() { + op := imgutil.WithPurge(true) + h.AssertNil(t, op(pushOptions)) + h.AssertEq(t, pushOptions.Purge, true) + }) + it("#WithFormat", func() { + format := types.OCIImageIndex + op := imgutil.WithFormat(format) + h.AssertNil(t, op(pushOptions)) + h.AssertEq(t, pushOptions.Format, format) + }) + it("#WithFormat error", func() { + op := imgutil.WithFormat(types.OCIConfigJSON) + h.AssertNotEq(t, op(pushOptions), nil) + h.AssertEq(t, pushOptions.Format, types.MediaType("")) + }) + it("#WithTags", func() { + tags := []string{"latest", "0.0.1", "1.0.0"} + op := imgutil.WithTags(tags...) + h.AssertNil(t, op(pushOptions)) + h.AssertEq(t, pushOptions.Tags, tags) + }) + }) +} diff --git a/remote/index.go b/remote/index.go deleted file mode 100644 index 5704aee8..00000000 --- a/remote/index.go +++ /dev/null @@ -1,150 +0,0 @@ -package remote - -import ( - "fmt" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/match" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/types" - "github.com/pkg/errors" - - "github.com/buildpacks/imgutil" -) - -type ImageIndex struct { - keychain authn.Keychain - repoName string - index v1.ImageIndex - registrySettings map[string]registrySetting -} - -// modfiers - -// Add appends a new image manifest to the remote ImageIndex/ManifestList. -// We have not implemented nested indexes yet. -// See specification for more info: -// https://github.com/opencontainers/image-spec/blob/0b40f0f367c396cc5a7d6a2e8c8842271d3d3844/image-index.md#image-index-property-descriptions -func (i *ImageIndex) Add(repoName string) error { - ref, err := name.ParseReference(repoName) - if err != nil { - return err - } - - // Fetch image descriptor from registry - desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return errors.Wrapf(err, "error fetching %s from registry", repoName) - } - - img, err := desc.Image() - if err != nil { - return err - } - - // Get the image configuration file - cfg, err := img.ConfigFile() - - if err != nil { - return errors.Wrapf(err, "getting config file for image %q", repoName) - } - if cfg == nil { - return fmt.Errorf("missing config for image %q", repoName) - } - - platform := v1.Platform{} - platform.Architecture = cfg.Architecture - platform.OS = cfg.OS - - desc.Descriptor.Platform = &platform - - i.index = mutate.AppendManifests(i.index, mutate.IndexAddendum{Add: img, Descriptor: desc.Descriptor}) - - return nil -} - -// Remove method removes the specified manifest from the index -func (i *ImageIndex) Remove(repoName string) error { - ref, err := name.ParseReference(repoName) - if err != nil { - return err - } - - desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) - if err != nil { - return err - } - - i.index = mutate.RemoveManifests(i.index, match.Digests(desc.Digest)) - - return nil -} - -// Save pushes the ImageIndex to the image reference obtained from index name. -func (i *ImageIndex) Save(additionalNames ...string) error { - return i.SaveAs(i.Name(), additionalNames...) -} - -func (i *ImageIndex) SaveAs(name string, additionalNames ...string) error { - allNames := append([]string{name}, additionalNames...) - - var diagnostics []imgutil.SaveIndexDiagnostic - for _, n := range allNames { - if err := i.doSave(n); err != nil { - diagnostics = append(diagnostics, imgutil.SaveIndexDiagnostic{ImageIndexName: n, Cause: err}) - } - } - if len(diagnostics) > 0 { - return imgutil.SaveIndexError{Errors: diagnostics} - } - - return nil -} - -func (i *ImageIndex) doSave(indexName string) error { - reg := getRegistry(i.repoName, i.registrySettings) - ref, auth, err := referenceForRepoName(i.keychain, indexName, reg.insecure) - if err != nil { - return err - } - - iManifest, err := i.index.IndexManifest() - if err != nil { - return err - } - - // This for loop will check if all the referenced manifests have the plaform information. - // This is OPTIONAL if the target is plaform independent. - // Current implementation does not allow to push an index without platform information. - for _, j := range iManifest.Manifests { - switch j.MediaType { - case types.OCIManifestSchema1, types.DockerManifestSchema2: - if j.Platform.Architecture == "" || j.Platform.OS == "" { - return errors.Errorf("manifest with digest %s is missing either OS or Architecture information to be pushed to a registry", j.Digest) - } - } - } - - return remote.WriteIndex(ref, i.index, remote.WithAuth(auth)) -} - -func (i *ImageIndex) Name() string { - return i.repoName -} - -// This structure is used to expose methods that we only need for testing. -type ImageIndexTest struct { - ImageIndex -} - -func (i *ImageIndexTest) MediaType() (types.MediaType, error) { - mediaType, err := i.ImageIndex.index.MediaType() - if err != nil { - return "", err - } - - return mediaType, nil -} diff --git a/remote/index_options.go b/remote/index_options.go deleted file mode 100644 index 4c13f135..00000000 --- a/remote/index_options.go +++ /dev/null @@ -1,30 +0,0 @@ -package remote - -import ( - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/buildpacks/imgutil" -) - -type ImageIndexOption func(*indexOptions) error - -type indexOptions struct { - mediaTypes imgutil.MediaTypes - manifest v1.IndexManifest -} - -// WithIndexMediaTypes lets a caller set the desired media types for the index manifest -func WithIndexMediaTypes(requested imgutil.MediaTypes) ImageIndexOption { - return func(opts *indexOptions) error { - opts.mediaTypes = requested - return nil - } -} - -// WithManifest uses an existing v1.IndexManifest as a base to create the index -func WithManifest(manifest v1.IndexManifest) ImageIndexOption { - return func(opts *indexOptions) error { - opts.manifest = manifest - return nil - } -} diff --git a/remote/index_test.go b/remote/index_test.go deleted file mode 100644 index ea99fc2d..00000000 --- a/remote/index_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package remote_test - -import ( - "fmt" - "io" - "log" - "os" - "strings" - "testing" - - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/registry" - "github.com/google/go-containerregistry/pkg/v1/types" - "github.com/sclevine/spec" - "github.com/sclevine/spec/report" - - "github.com/buildpacks/imgutil" - "github.com/buildpacks/imgutil/remote" - h "github.com/buildpacks/imgutil/testhelpers" -) - -func newTestIndexName(providedPrefix ...string) string { - prefix := "pack-index-test" - if len(providedPrefix) > 0 { - prefix = providedPrefix[0] - } - - return dockerRegistry.RepoName(prefix + "-" + h.RandString(10)) -} - -func TestIndex(t *testing.T) { - dockerConfigDir, err := os.MkdirTemp("", "test.docker.config.dir") - h.AssertNil(t, err) - defer os.RemoveAll(dockerConfigDir) - - sharedRegistryHandler := registry.New(registry.Logger(log.New(io.Discard, "", log.Lshortfile))) - dockerRegistry = h.NewDockerRegistry(h.WithAuth(dockerConfigDir), h.WithSharedHandler(sharedRegistryHandler)) - - dockerRegistry.SetInaccessible("cnbs/no-image-in-this-name") - - dockerRegistry.Start(t) - defer dockerRegistry.Stop(t) - - os.Setenv("DOCKER_CONFIG", dockerRegistry.DockerDirectory) - defer os.Unsetenv("DOCKER_CONFIG") - - spec.Run(t, "Index", testIndex, spec.Sequential(), spec.Report(report.Terminal{})) -} - -func testIndex(t *testing.T, when spec.G, it spec.S) { - when("#NewIndex", func() { - when("index name is invalid", func() { - it("return error", func() { - _, err := remote.NewIndex("-.bad-@!mage", authn.DefaultKeychain) - h.AssertError(t, err, "could not parse reference: -.bad-@!mage") - }) - }) - - when("index name is valid", func() { - it("create index with the specified name", func() { - image := newTestIndexName() - idxt, err := remote.NewIndex(image, authn.DefaultKeychain) - h.AssertNil(t, err) - h.AssertEq(t, image, idxt.Name()) - }) - }) - - when("no options specified", func() { - it("uses DockerManifestList as default mediatype", func() { - idxt, _ := remote.NewIndexTest(newTestIndexName(), authn.DefaultKeychain) - mediatype, _ := idxt.MediaType() - h.AssertEq(t, mediatype, types.DockerManifestList) - }) - }) - - when("when index is found in registry", func() { - it("use the index found in registry as base", func() { - }) - }) - - when("#WithIndexMediaTypes", func() { - it("create index with the specified mediatype", func() { - idxt, err := remote.NewIndexTest( - newTestIndexName(), - authn.DefaultKeychain, - remote.WithIndexMediaTypes(imgutil.OCITypes)) - h.AssertNil(t, err) - - mediatype, err := idxt.MediaType() - h.AssertNil(t, err) - h.AssertEq(t, mediatype, types.OCIImageIndex) - }) - }) - }) - - when("#Add", func() { - when("manifest is not in registry", func() { - it("error (timeout) fetching manifest", func() { - idx, err := remote.NewIndex("cnbs/test-index", authn.DefaultKeychain) - h.AssertNil(t, err) - - manifestName := dockerRegistry.RepoName("cnbs/no-image-in-this-name") - err = idx.Add(manifestName) - h.AssertError(t, err, fmt.Sprintf("error fetching %s from registry", manifestName)) - }) - }) - - when("manifest name is invalid", func() { - it("error parsing reference", func() { - idx, err := remote.NewIndex("some-bad-repo", authn.DefaultKeychain) - h.AssertNil(t, err) - - manifestName := dockerRegistry.RepoName("cnbs/bad-@!mage") - err = idx.Add(manifestName) - h.AssertError(t, err, fmt.Sprintf("could not parse reference: %s", manifestName)) - }) - }) - - when("manifest is in registry", func() { - it("append manifest to index", func() { - idx, err := remote.NewIndex("cnbs/test-index", authn.DefaultKeychain) - h.AssertNil(t, err) - - manifestName := dockerRegistry.RepoName("cnbs/test-image:arm") - img, err := remote.NewImage( - manifestName, - authn.DefaultKeychain, - remote.WithDefaultPlatform(imgutil.Platform{ - Architecture: "arm", - OS: "linux", - }), - ) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - - err = idx.Add(manifestName) - h.AssertNil(t, err) - }) - }) - }) - - when("#Save", func() { - when("manifest plaform fields are missing", func() { - it("error storing in registry", func() { - indexName := dockerRegistry.RepoName("cnbs/test-index-not-valid") - idx, err := remote.NewIndex(indexName, authn.DefaultKeychain) - h.AssertNil(t, err) - - manifestName := dockerRegistry.RepoName("cnbs/test-image:arm") - img, err := remote.NewImage( - manifestName, - authn.DefaultKeychain, - remote.WithDefaultPlatform(imgutil.Platform{ - Architecture: "", - OS: "linux", - }), - ) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - - h.AssertNil(t, idx.Add(manifestName)) - - a := strings.Split(idx.Save().Error(), " ") - - h.AssertContains(t, a, "missing", "OS", "Architecture") - }) - }) - - when("index is valid to push", func() { - it("store index in registry", func() { - indexName := dockerRegistry.RepoName("cnbs/test-index-valid") - idx, err := remote.NewIndex(indexName, authn.DefaultKeychain) - h.AssertNil(t, err) - - manifestName := dockerRegistry.RepoName("cnbs/test-image:arm-linux") - img, err := remote.NewImage( - manifestName, - authn.DefaultKeychain, - remote.WithDefaultPlatform(imgutil.Platform{ - Architecture: "arm", - OS: "linux", - }), - ) - h.AssertNil(t, err) - h.AssertNil(t, img.Save()) - - h.AssertNil(t, idx.Add(manifestName)) - - h.AssertNil(t, idx.Save()) - }) - }) - }) -} diff --git a/remote/new.go b/remote/new.go index a392de57..8af398e9 100644 --- a/remote/new.go +++ b/remote/new.go @@ -18,9 +18,51 @@ import ( "github.com/pkg/errors" "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" "github.com/buildpacks/imgutil/layer" ) +// NewIndex returns a new ImageIndex from the registry that can be modified and saved to local file system +func NewIndex(repoName string, ops ...index.Option) (idx imgutil.ImageIndex, err error) { + var idxOps = &index.Options{} + ops = append(ops, index.WithRepoName(repoName)) + + for _, op := range ops { + err = op(idxOps) + if err != nil { + return + } + } + + ref, err := name.ParseReference(idxOps.RepoName(), name.WeakValidation, name.Insecure) + if err != nil { + return + } + + desc, err := remote.Get( + ref, + remote.WithAuthFromKeychain(idxOps.Keychain()), + remote.WithTransport(imgutil.GetTransport(idxOps.Insecure())), + ) + if err != nil { + return + } + + imgIdx, err := desc.ImageIndex() + if err != nil { + return idx, err + } + + idxOptions := imgutil.IndexOptions{ + KeyChain: idxOps.Keychain(), + XdgPath: idxOps.XDGRuntimePath(), + Reponame: idxOps.RepoName(), + InsecureRegistry: idxOps.Insecure(), + } + + return imgutil.NewManifestHandler(imgIdx, idxOptions), nil +} + // NewImage returns a new Image that can be modified and saved to a Docker daemon. func NewImage(repoName string, keychain authn.Keychain, ops ...ImageOption) (*Image, error) { imageOpts := &options{} @@ -31,7 +73,7 @@ func NewImage(repoName string, keychain authn.Keychain, ops ...ImageOption) (*Im } platform := defaultPlatform() - if (imageOpts.platform != imgutil.Platform{}) { + if !platform.Satisfies(imageOpts.platform) { platform = imageOpts.platform } @@ -91,19 +133,21 @@ func NewImage(repoName string, keychain authn.Keychain, ops ...ImageOption) (*Im return ri, nil } -func defaultPlatform() imgutil.Platform { - return imgutil.Platform{ +func defaultPlatform() v1.Platform { + return v1.Platform{ OS: "linux", Architecture: runtime.GOARCH, } } -func emptyImage(platform imgutil.Platform) (v1.Image, error) { +func emptyImage(platform v1.Platform) (v1.Image, error) { cfg := &v1.ConfigFile{ Architecture: platform.Architecture, History: []v1.History{}, OS: platform.OS, OSVersion: platform.OSVersion, + Variant: platform.Variant, + OSFeatures: platform.OSFeatures, RootFS: v1.RootFS{ Type: "layers", DiffIDs: []v1.Hash{}, @@ -143,7 +187,7 @@ func prepareNewWindowsImage(ri *Image) error { return nil } -func processPreviousImageOption(ri *Image, prevImageRepoName string, platform imgutil.Platform) error { +func processPreviousImageOption(ri *Image, prevImageRepoName string, platform v1.Platform) error { reg := getRegistry(prevImageRepoName, ri.registrySettings) prevImage, err := NewV1Image(prevImageRepoName, ri.keychain, WithV1DefaultPlatform(platform), WithV1RegistrySetting(reg.insecure)) @@ -190,7 +234,7 @@ func NewV1Image(baseImageRepoName string, keychain authn.Keychain, ops ...V1Imag } platform := defaultPlatform() - if (imageOpts.platform != imgutil.Platform{}) { + if !platform.Satisfies(imageOpts.platform) { platform = imageOpts.platform } @@ -206,25 +250,19 @@ func NewV1Image(baseImageRepoName string, keychain authn.Keychain, ops ...V1Imag return baseImage, nil } -func newV1Image(keychain authn.Keychain, repoName string, platform imgutil.Platform, reg registrySetting) (v1.Image, error) { +func newV1Image(keychain authn.Keychain, repoName string, platform v1.Platform, reg registrySetting) (v1.Image, error) { ref, auth, err := referenceForRepoName(keychain, repoName, reg.insecure) if err != nil { return nil, err } - v1Platform := v1.Platform{ - Architecture: platform.Architecture, - OS: platform.OS, - OSVersion: platform.OSVersion, - } - var image v1.Image for i := 0; i <= maxRetries; i++ { time.Sleep(100 * time.Duration(i) * time.Millisecond) // wait if retrying image, err = remote.Image(ref, remote.WithAuth(auth), - remote.WithPlatform(v1Platform), - remote.WithTransport(getTransport(reg.insecure)), + remote.WithPlatform(platform), + remote.WithTransport(imgutil.GetTransport(reg.insecure)), ) if err != nil { if err == io.EOF && i != maxRetries { @@ -265,7 +303,7 @@ func referenceForRepoName(keychain authn.Keychain, ref string, insecure bool) (n return r, auth, nil } -func processBaseImageOption(ri *Image, baseImageRepoName string, platform imgutil.Platform) error { +func processBaseImageOption(ri *Image, baseImageRepoName string, platform v1.Platform) error { reg := getRegistry(baseImageRepoName, ri.registrySettings) var err error ri.image, err = NewV1Image(baseImageRepoName, ri.keychain, WithV1DefaultPlatform(platform), WithV1RegistrySetting(reg.insecure)) diff --git a/remote/new_index.go b/remote/new_index.go deleted file mode 100644 index 28815a18..00000000 --- a/remote/new_index.go +++ /dev/null @@ -1,113 +0,0 @@ -package remote - -import ( - "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/types" - - "github.com/buildpacks/imgutil" -) - -func NewIndex(repoName string, keychain authn.Keychain, ops ...ImageIndexOption) (*ImageIndex, error) { - ref, err := name.ParseReference(repoName, name.WeakValidation) - if err != nil { - return nil, err - } - - indexOpts := &indexOptions{} - for _, op := range ops { - if err := op(indexOpts); err != nil { - return nil, err - } - } - - // If WithManifest option is given, create an index using - // the provided v1.IndexManifest - if len(indexOpts.manifest.Manifests) != 0 { - index, err := emptyIndex(indexOpts.manifest.MediaType) - if err != nil { - return nil, err - } - - for _, manifest := range indexOpts.manifest.Manifests { - img, err := emptyImage(imgutil.Platform{ - Architecture: manifest.Platform.Architecture, - OS: manifest.Platform.OS, - OSVersion: manifest.Platform.OSVersion, - }) - if err != nil { - return nil, err - } - - index = mutate.AppendManifests(index, mutate.IndexAddendum{Add: img, Descriptor: manifest}) - } - - idx := &ImageIndex{ - keychain: keychain, - repoName: repoName, - index: index, - } - - return idx, nil - } - - // If index already exists in registry, use it as a base - desc, err := remote.Get(ref, remote.WithAuthFromKeychain(keychain)) - if err == nil { - index, err := desc.ImageIndex() - if err != nil { - return nil, err - } - - idx := &ImageIndex{ - keychain: keychain, - repoName: repoName, - index: index, - } - - return idx, nil - } - - mediaType := defaultMediaType() - if indexOpts.mediaTypes.IndexManifestType() != "" { - mediaType = indexOpts.mediaTypes - } - - index, err := emptyIndex(mediaType.IndexManifestType()) - if err != nil { - return nil, err - } - - ridx := &ImageIndex{ - keychain: keychain, - repoName: repoName, - index: index, - } - - return ridx, nil -} - -func emptyIndex(mediaType types.MediaType) (v1.ImageIndex, error) { - return mutate.IndexMediaType(empty.Index, mediaType), nil -} - -func defaultMediaType() imgutil.MediaTypes { - return imgutil.DockerTypes -} - -func NewIndexTest(repoName string, keychain authn.Keychain, ops ...ImageIndexOption) (*ImageIndexTest, error) { - ridx, err := NewIndex(repoName, keychain, ops...) - if err != nil { - return nil, err - } - - ridxt := &ImageIndexTest{ - ImageIndex: *ridx, - } - - return ridxt, nil -} diff --git a/remote/new_test.go b/remote/new_test.go new file mode 100644 index 00000000..b185c4e9 --- /dev/null +++ b/remote/new_test.go @@ -0,0 +1,124 @@ +package remote_test + +import ( + "os" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" + "github.com/buildpacks/imgutil/remote" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestRemoteNew(t *testing.T) { + spec.Run(t, "RemoteNew", testRemoteNew, spec.Sequential(), spec.Report(report.Terminal{})) +} + +func testRemoteNew(t *testing.T, when spec.G, it spec.S) { + var ( + xdgPath = "xdgPath" + ) + when("#NewIndex", func() { + it.After(func() { + err := os.RemoveAll(xdgPath) + h.AssertNil(t, err) + }) + it("should have expected indexOptions", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + h.AssertEq(t, imgIx.Options.Insecure(), true) + h.AssertEq(t, imgIx.Options.XdgPath, xdgPath) + h.AssertEq(t, imgIx.Options.Reponame, "busybox:1.36-musl") + }) + it("should return an error when invalid repoName is passed", func() { + _, err := remote.NewIndex( + "some/invalidImage", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertEq(t, err.Error(), "could not parse reference: some/invalidImage") + }) + it("should return an error when index with the given repoName doesn't exists", func() { + _, err := remote.NewIndex( + "some/image", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNotEq(t, err, nil) + }) + it("should return ImageIndex with expected output", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + mfest, err := imgIx.IndexManifest() + h.AssertNil(t, err) + h.AssertNotEq(t, mfest, nil) + h.AssertEq(t, len(mfest.Manifests), 8) + }) + it("should able to call #ImageIndex", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + // linux/amd64 + hash1, err := v1.NewHash( + "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + ) + h.AssertNil(t, err) + + _, err = imgIx.ImageIndex.ImageIndex(hash1) + h.AssertNotEq(t, err.Error(), "empty index") + }) + it("should able to call #Image", func() { + idx, err := remote.NewIndex( + "busybox:1.36-musl", + index.WithInsecure(true), + index.WithKeychain(authn.DefaultKeychain), + index.WithXDGRuntimePath(xdgPath), + ) + h.AssertNil(t, err) + + imgIdx, ok := idx.(*imgutil.ManifestHandler) + h.AssertEq(t, ok, true) + + // linux/amd64 + hash1, err := v1.NewHash( + "sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34", + ) + h.AssertNil(t, err) + + _, err = imgIdx.Image(hash1) + h.AssertNil(t, err) + }) + }) +} diff --git a/remote/options.go b/remote/options.go index a1cd8294..6065d77c 100644 --- a/remote/options.go +++ b/remote/options.go @@ -11,7 +11,7 @@ import ( type ImageOption func(*options) error type options struct { - platform imgutil.Platform + platform v1.Platform baseImageRepoName string prevImageRepoName string createdAt time.Time @@ -60,9 +60,9 @@ func WithConfig(config *v1.Config) ImageOption { // WithDefaultPlatform provides Architecture/OS/OSVersion defaults for the new image. // Defaults for a new image are ignored when FromBaseImage returns an image. // FromBaseImage and WithPreviousImage will use the platform to choose an image from a manifest list. -func WithDefaultPlatform(platform imgutil.Platform) ImageOption { +func WithDefaultPlatform(platform v1.Platform) ImageOption { return func(opts *options) error { - opts.platform = platform + platform.DeepCopyInto(&opts.platform) return nil } } @@ -114,14 +114,14 @@ func WithRegistrySetting(repository string, insecure bool) ImageOption { // v1Options is used to configure the behavior when a v1.Image is created type v1Options struct { - platform imgutil.Platform + platform v1.Platform registrySetting registrySetting } type V1ImageOption func(*v1Options) error // WithV1DefaultPlatform provides Architecture/OS/OSVersion defaults for the new v1.Image. -func WithV1DefaultPlatform(platform imgutil.Platform) V1ImageOption { +func WithV1DefaultPlatform(platform v1.Platform) V1ImageOption { return func(opts *v1Options) error { opts.platform = platform return nil diff --git a/remote/remote.go b/remote/remote.go index 581593f7..d2e157e4 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -24,16 +24,19 @@ import ( const maxRetries = 2 type Image struct { - keychain authn.Keychain - repoName string - image v1.Image - prevLayers []v1.Layer - prevHistory []v1.History - createdAt time.Time - addEmptyLayerOnSave bool - withHistory bool - registrySettings map[string]registrySetting - requestedMediaTypes imgutil.MediaTypes + keychain authn.Keychain + repoName string + image v1.Image + prevLayers []v1.Layer + prevHistory []v1.History + createdAt time.Time + addEmptyLayerOnSave bool + withHistory bool + registrySettings map[string]registrySetting + requestedMediaTypes imgutil.MediaTypes + os, arch, variant, osVersion string + features, osFeatures, urls []string + annotations map[string]string } type registrySetting struct { @@ -43,6 +46,10 @@ type registrySetting struct { // getters func (i *Image) Architecture() (string, error) { + if i.arch != "" { + return i.arch, nil + } + cfg, err := i.image.ConfigFile() if err != nil { return "", errors.Wrapf(err, "getting config file for image %q", i.repoName) @@ -104,7 +111,7 @@ func (i *Image) found() (*v1.Descriptor, error) { if err != nil { return nil, err } - return remote.Head(ref, remote.WithAuth(auth), remote.WithTransport(getTransport(reg.insecure))) + return remote.Head(ref, remote.WithAuth(auth), remote.WithTransport(imgutil.GetTransport(reg.insecure))) } func (i *Image) Valid() bool { @@ -117,7 +124,7 @@ func (i *Image) valid() error { if err != nil { return err } - desc, err := remote.Get(ref, remote.WithAuth(auth), remote.WithTransport(getTransport(reg.insecure))) + desc, err := remote.Get(ref, remote.WithAuth(auth), remote.WithTransport(imgutil.GetTransport(reg.insecure))) if err != nil { return err } @@ -220,6 +227,10 @@ func (i *Image) Name() string { } func (i *Image) OS() (string, error) { + if i.os != "" { + return i.os, nil + } + cfg, err := i.image.ConfigFile() if err != nil { return "", errors.Wrapf(err, "getting config file for image %q", i.repoName) @@ -234,6 +245,10 @@ func (i *Image) OS() (string, error) { } func (i *Image) OSVersion() (string, error) { + if i.osVersion != "" { + return i.osVersion, nil + } + cfg, err := i.image.ConfigFile() if err != nil { return "", errors.Wrapf(err, "getting config file for image %q", i.repoName) @@ -244,6 +259,113 @@ func (i *Image) OSVersion() (string, error) { return cfg.OSVersion, nil } +func (i *Image) Features() (features []string, err error) { + if len(i.features) != 0 { + return i.features, nil + } + + mfest, err := i.image.Manifest() + if err != nil { + return features, errors.Wrapf(err, "getting image manifest for image %q", i.repoName) + } + if mfest == nil { + return features, fmt.Errorf("missing manifest for image %q", i.repoName) + } + + p := mfest.Config.Platform + if p == nil { + p = &v1.Platform{} + } + + subject := mfest.Subject + if subject == nil { + subject = &v1.Descriptor{} + } + + subjectPlatform := subject.Platform + if subjectPlatform == nil { + subjectPlatform = &v1.Platform{} + } + + switch { + case len(p.Features) != 0: + return p.Features, nil + case len(subjectPlatform.Features) != 0: + return subjectPlatform.Features, nil + default: + return features, imgutil.ErrFeaturesUndefined(i.requestedMediaTypes.ManifestType(), i.repoName) + } +} + +func (i *Image) OSFeatures() (osFeatures []string, err error) { + if len(i.osFeatures) != 0 { + return i.osFeatures, nil + } + + cfg, err := i.image.ConfigFile() + if err != nil { + return osFeatures, errors.Wrapf(err, "getting config file for image %q", i.repoName) + } + if cfg == nil { + return osFeatures, fmt.Errorf("missing config for image %q", i.repoName) + } + if len(cfg.OSFeatures) < 1 { + return osFeatures, imgutil.ErrFeaturesUndefined(i.requestedMediaTypes.ManifestType(), i.repoName) + } + + return cfg.OSFeatures, nil +} + +func (i *Image) URLs() (urls []string, err error) { + if len(i.urls) != 0 { + return i.urls, nil + } + + mfest, err := i.image.Manifest() + if err != nil { + return urls, err + } + + if mfest == nil { + return urls, imgutil.ErrManifestUndefined + } + + subject := mfest.Subject + if subject == nil { + subject = &v1.Descriptor{} + } + + switch { + case len(mfest.Config.URLs) != 0: + return mfest.Config.URLs, nil + case len(subject.URLs) != 0: + return subject.URLs, nil + default: + return urls, imgutil.ErrURLsUndefined(i.requestedMediaTypes.ManifestType(), i.repoName) + } +} + +func (i *Image) Annotations() (annos map[string]string, err error) { + if len(i.annotations) != 0 { + return i.annotations, nil + } + + mfest, err := i.image.Manifest() + if err != nil { + return annos, err + } + + if mfest == nil { + return annos, imgutil.ErrManifestUndefined + } + + if len(mfest.Annotations) < 1 { + return annos, imgutil.ErrAnnotationsUndefined(i.requestedMediaTypes.ManifestType(), i.repoName) + } + + return mfest.Annotations, nil +} + func (i *Image) TopLayer() (string, error) { all, err := i.image.Layers() if err != nil { @@ -261,6 +383,10 @@ func (i *Image) TopLayer() (string, error) { } func (i *Image) Variant() (string, error) { + if i.variant != "" { + return i.variant, nil + } + cfg, err := i.image.ConfigFile() if err != nil { return "", errors.Wrapf(err, "getting config file for image %q", i.repoName) @@ -294,6 +420,7 @@ func (i *Image) Rename(name string) { } func (i *Image) SetArchitecture(architecture string) error { + i.arch = architecture configFile, err := i.image.ConfigFile() if err != nil { return err @@ -376,6 +503,7 @@ func (i *Image) SetLabel(key, val string) error { } func (i *Image) SetOS(osVal string) error { + i.os = osVal configFile, err := i.image.ConfigFile() if err != nil { return err @@ -386,6 +514,7 @@ func (i *Image) SetOS(osVal string) error { } func (i *Image) SetOSVersion(osVersion string) error { + i.osVersion = osVersion configFile, err := i.image.ConfigFile() if err != nil { return err @@ -396,6 +525,7 @@ func (i *Image) SetOSVersion(osVersion string) error { } func (i *Image) SetVariant(variant string) error { + i.variant = variant configFile, err := i.image.ConfigFile() if err != nil { return err @@ -405,6 +535,51 @@ func (i *Image) SetVariant(variant string) error { return err } +func (i *Image) SetOSFeatures(osFeatures []string) error { + i.osFeatures = append(i.osFeatures, osFeatures...) + configFile, err := i.image.ConfigFile() + if err != nil { + return err + } + + if configFile == nil { + return imgutil.ErrConfigFileUndefined + } + + configFile.OSFeatures = osFeatures + i.image, err = mutate.ConfigFile(i.image, configFile) + return err +} + +func (i *Image) SetFeatures(features []string) error { + i.features = append(i.features, features...) + return nil +} + +func (i *Image) SetURLs(urls []string) error { + i.urls = append(i.urls, urls...) + return nil +} + +func (i *Image) SetAnnotations(annos map[string]string) error { + if len(i.annotations) == 0 { + i.annotations = make(map[string]string) + } + + for k, v := range annos { + i.annotations[k] = v + } + return nil +} + +func (i *Image) Digest() (v1.Hash, error) { + return i.image.Digest() +} + +func (i *Image) MediaType() (types.MediaType, error) { + return i.image.MediaType() +} + func (i *Image) SetWorkingDir(dir string) error { configFile, err := i.image.ConfigFile() if err != nil { @@ -458,7 +633,7 @@ func (i *Image) Delete() error { if err != nil { return err } - return remote.Delete(ref, remote.WithAuth(auth), remote.WithTransport(getTransport(reg.insecure))) + return remote.Delete(ref, remote.WithAuth(auth), remote.WithTransport(imgutil.GetTransport(reg.insecure))) } func (i *Image) Rebase(baseTopLayer string, newBase imgutil.Image) error { diff --git a/remote/remote_test.go b/remote/remote_test.go index a89798f1..9ed4ab57 100644 --- a/remote/remote_test.go +++ b/remote/remote_test.go @@ -119,7 +119,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { img, err := remote.NewImage( newTestImageName(), authn.DefaultKeychain, - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ Architecture: "arm", OS: "windows", OSVersion: "10.0.17763.316", @@ -151,7 +151,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { img, err := remote.NewImage( newTestImageName(), authn.DefaultKeychain, - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ Architecture: "arm", OS: "linux", }), @@ -310,7 +310,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, authn.DefaultKeychain, remote.FromBaseImage(windowsImageManifestName), - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ Architecture: "amd64", OS: "windows", OSVersion: "10.0.17763.1397", @@ -341,7 +341,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, authn.DefaultKeychain, remote.FromBaseImage(windowsImageManifestName), - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ OS: "linux", Architecture: "arm", }), @@ -372,7 +372,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, authn.DefaultKeychain, remote.FromBaseImage(manifestListName), - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ OS: "linux", Architecture: "amd64", }), @@ -397,7 +397,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, authn.DefaultKeychain, remote.FromBaseImage(manifestListName), - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ OS: "windows", Architecture: "arm", }), @@ -432,7 +432,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, authn.DefaultKeychain, remote.FromBaseImage("some-bad-repo-name"), - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ Architecture: "arm", OS: "linux", }), @@ -461,7 +461,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, authn.DefaultKeychain, remote.FromBaseImage("some-bad-repo-name"), - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ Architecture: "arm", OS: "windows", OSVersion: "10.0.99999.9999", @@ -518,7 +518,7 @@ func testImage(t *testing.T, when spec.G, it spec.S) { repoName, authn.DefaultKeychain, remote.WithPreviousImage(manifestListName), - remote.WithDefaultPlatform(imgutil.Platform{ + remote.WithDefaultPlatform(v1.Platform{ OS: "windows", Architecture: "amd64", }), @@ -1129,24 +1129,52 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) }) - when("#SetOS #SetOSVersion #SetArchitecture", func() { + when("#SetOS #SetOSVersion #SetArchitecture #SetVariant #SetFeatures #SetOSFeatures #SetURLs #SetAnnotations", func() { it("sets the os/arch", func() { + var ( + os = "foobaros" + arch = "arm64" + osVersion = "1.2.3.4" + variant = "some-variant" + features = []string{"some-features"} + osFeatures = []string{"some-osFeatures"} + urls = []string{"some-urls"} + annos = map[string]string{"some-key": "some-value"} + ) img, err := remote.NewImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) - err = img.SetOS("foobaros") + err = img.SetOS(os) h.AssertNil(t, err) - err = img.SetOSVersion("1.2.3.4") + err = img.SetOSVersion(osVersion) h.AssertNil(t, err) - err = img.SetArchitecture("arm64") + err = img.SetArchitecture(arch) h.AssertNil(t, err) + h.AssertNil(t, img.SetVariant(variant)) + h.AssertNil(t, img.SetOSFeatures(osFeatures)) + h.AssertNil(t, img.SetAnnotations(annos)) + h.AssertNil(t, img.SetFeatures(features)) + h.AssertNil(t, img.SetURLs(urls)) h.AssertNil(t, img.Save()) configFile := h.FetchManifestImageConfigFile(t, repoName) - h.AssertEq(t, configFile.OS, "foobaros") - h.AssertEq(t, configFile.OSVersion, "1.2.3.4") - h.AssertEq(t, configFile.Architecture, "arm64") + h.AssertEq(t, configFile.OS, os) + h.AssertEq(t, configFile.OSVersion, osVersion) + h.AssertEq(t, configFile.Architecture, arch) + h.AssertEq(t, configFile.Variant, variant) + h.AssertEq(t, configFile.OSFeatures, osFeatures) + + mfest := h.FetchImageManifest(t, repoName) + + h.AssertEq(t, mfest.Subject.Platform.OS, os) + h.AssertEq(t, mfest.Subject.Platform.Architecture, arch) + h.AssertEq(t, mfest.Subject.Platform.Variant, variant) + h.AssertEq(t, mfest.Subject.Platform.OSVersion, osVersion) + h.AssertEq(t, mfest.Subject.Platform.Features, features) + h.AssertEq(t, mfest.Subject.Platform.OSFeatures, osFeatures) + h.AssertEq(t, mfest.Subject.URLs, urls) + h.AssertEq(t, mfest.Annotations, map[string]string{"some-key": "some-value"}) }) }) diff --git a/remote/save.go b/remote/save.go index e30de4df..88b526ad 100644 --- a/remote/save.go +++ b/remote/save.go @@ -1,9 +1,7 @@ package remote import ( - "crypto/tls" "fmt" - "net/http" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -71,6 +69,51 @@ func (i *Image) SaveAs(name string, additionalNames ...string) error { } } + i.image, err = imgutil.MutateManifest(i.image, func(mfest *v1.Manifest) { + config := mfest.Config + if len(i.annotations) != 0 { + mfest.Annotations = i.annotations + config.Annotations = i.annotations + } + + if len(i.urls) != 0 { + config.URLs = append(config.URLs, i.urls...) + } + + if config.Platform == nil { + config.Platform = &v1.Platform{} + } + + if len(i.features) != 0 { + config.Platform.Features = append(config.Platform.Features, i.features...) + } + + if len(i.osFeatures) != 0 { + config.Platform.OSFeatures = append(config.Platform.OSFeatures, i.osFeatures...) + } + + if i.os != "" { + config.Platform.OS = i.os + } + + if i.arch != "" { + config.Platform.Architecture = i.arch + } + + if i.variant != "" { + config.Platform.Variant = i.variant + } + + if i.osVersion != "" { + config.Platform.OSVersion = i.osVersion + } + + mfest.Config = config + }) + if err != nil { + return err + } + // save var diagnostics []imgutil.SaveDiagnostic for _, n := range allNames { @@ -94,19 +137,6 @@ func (i *Image) doSave(imageName string) error { return remote.Write(ref, i.image, remote.WithAuth(auth), - remote.WithTransport(getTransport(reg.insecure)), + remote.WithTransport(imgutil.GetTransport(reg.insecure)), ) } - -func getTransport(insecure bool) http.RoundTripper { - // #nosec G402 - if insecure { - return &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - } - - return http.DefaultTransport -} diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 288bae5a..4ab5d242 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -45,14 +45,14 @@ func RandString(n int) string { } // AssertEq asserts deep equality (and provides a useful difference as a test failure) -func AssertEq(t *testing.T, actual, expected interface{}) { +func AssertEq(t *testing.T, actual, expected any) { t.Helper() if diff := cmp.Diff(actual, expected); diff != "" { t.Fatal(diff) } } -func AssertNotEq(t *testing.T, v1, v2 interface{}) { +func AssertNotEq(t *testing.T, v1, v2 any) { t.Helper() if diff := cmp.Diff(v1, v2); diff == "" { @@ -109,13 +109,20 @@ func AssertError(t *testing.T, actual error, expected string) { } } -func AssertNil(t *testing.T, actual interface{}) { +func AssertNil(t *testing.T, actual any) { t.Helper() if actual != nil { t.Fatalf("Expected nil: %s", actual) } } +func AssertNotNil(t *testing.T, actual any) { + t.Helper() + if actual == nil { + t.Fatalf("Expected not nil: %s", actual) + } +} + func AssertBlobsLen(t *testing.T, path string, expected int) { t.Helper() fis, err := os.ReadDir(filepath.Join(path, "blobs", "sha256")) @@ -365,10 +372,30 @@ func FetchManifestImageConfigFile(t *testing.T, repoName string) *v1.ConfigFile configFile, err := gImg.ConfigFile() AssertNil(t, err) + AssertNotEq(t, configFile, nil) return configFile } +func FetchImageManifest(t *testing.T, repoName string) *v1.Manifest { + t.Helper() + + r, err := name.ParseReference(repoName, name.WeakValidation) + AssertNil(t, err) + + auth, err := authn.DefaultKeychain.Resolve(r.Context().Registry) + AssertNil(t, err) + + gImg, err := remote.Image(r, remote.WithTransport(http.DefaultTransport), remote.WithAuth(auth)) + AssertNil(t, err) + + mfest, err := gImg.Manifest() + AssertNil(t, err) + AssertNotEq(t, mfest, nil) + + return mfest +} + func FileDiffID(t *testing.T, path string) string { tarFile, err := os.Open(filepath.Clean(path)) AssertNil(t, err) diff --git a/util.go b/util.go new file mode 100644 index 00000000..5123574b --- /dev/null +++ b/util.go @@ -0,0 +1,529 @@ +package imgutil + +import ( + "encoding/json" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func MutateManifest(i v1.Image, withFunc func(c *v1.Manifest)) (v1.Image, error) { + // FIXME: put MutateManifest on the interface when `remote` and `layout` packages also support it. + digest, err := i.Digest() + if err != nil { + return nil, err + } + + mfest, err := getManifest(i) + if err != nil { + return nil, err + } + + config := mfest.Config + config.Digest = digest + config.MediaType = mfest.MediaType + if config.Size, err = partial.Size(i); err != nil { + return nil, err + } + config.Annotations = mfest.Annotations + + p := config.Platform + if p == nil { + p = &v1.Platform{} + } + + config.Platform = p + mfest.Config = config + + withFunc(mfest) + if len(mfest.Annotations) != 0 { + i = mutate.Annotations(i, mfest.Annotations).(v1.Image) + } + + return mutate.Subject(i, mfest.Config).(v1.Image), err +} + +// Any ImageIndex with RawManifest method. +type TaggableIndex struct { + *v1.IndexManifest +} + +// Returns the bytes of IndexManifest. +func (t *TaggableIndex) RawManifest() ([]byte, error) { + return json.Marshal(t.IndexManifest) +} + +// Returns the Digest of the IndexManifest if present. +// Else generate a new Digest. +func (t *TaggableIndex) Digest() (v1.Hash, error) { + if t.IndexManifest.Subject != nil && t.IndexManifest.Subject.Digest != (v1.Hash{}) { + return t.IndexManifest.Subject.Digest, nil + } + + return partial.Digest(t) +} + +// Returns the MediaType of the IndexManifest. +func (t *TaggableIndex) MediaType() (types.MediaType, error) { + return t.IndexManifest.MediaType, nil +} + +// Returns the Size of IndexManifest if present. +// Calculate the Size of empty. +func (t *TaggableIndex) Size() (int64, error) { + if t.IndexManifest.Subject != nil && t.IndexManifest.Subject.Size != 0 { + return t.IndexManifest.Subject.Size, nil + } + + return partial.Size(t) +} + +type StringSet struct { + items map[string]bool +} + +func (s *StringSet) Add(str string) { + if s == nil { + s = &StringSet{items: make(map[string]bool)} + } + + s.items[str] = true +} + +func (s *StringSet) Remove(str string) { + if s == nil { + s = &StringSet{items: make(map[string]bool)} + } + + s.items[str] = false +} + +func (s *StringSet) StringSlice() (slice []string) { + if s == nil { + s = &StringSet{items: make(map[string]bool)} + } + + for i, ok := range s.items { + if ok { + slice = append(slice, i) + } + } + + return slice +} + +// An helper struct used for keeping track of changes made to ImageIndex. +type Annotate struct { + Instance map[v1.Hash]v1.Descriptor +} + +// Returns `OS` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) OS(hash v1.Hash) (os string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc, ok := a.Instance[hash] + if !ok || desc.Platform == nil || desc.Platform.OS == "" { + return os, ErrOSUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.Platform.OS, nil +} + +// Sets the `OS` of an Image/ImageIndex to keep track of changes. +func (a *Annotate) SetOS(hash v1.Hash, os string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Platform.OS = os + a.Instance[hash] = desc +} + +// Returns `Architecture` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) Architecture(hash v1.Hash) (arch string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil || desc.Platform.Architecture == "" { + return arch, ErrArchUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.Platform.Architecture, nil +} + +// Annotates the `Architecture` of the given Image. +func (a *Annotate) SetArchitecture(hash v1.Hash, arch string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Platform.Architecture = arch + a.Instance[hash] = desc +} + +// Returns `Variant` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) Variant(hash v1.Hash) (variant string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil || desc.Platform.Variant == "" { + return variant, ErrVariantUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.Platform.Variant, nil +} + +// Annotates the `Variant` of the given Image. +func (a *Annotate) SetVariant(hash v1.Hash, variant string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Platform.Variant = variant + a.Instance[hash] = desc +} + +// Returns `OSVersion` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) OSVersion(hash v1.Hash) (osVersion string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil || desc.Platform.OSVersion == "" { + return osVersion, ErrOSVersionUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.Platform.OSVersion, nil +} + +// Annotates the `OSVersion` of the given Image. +func (a *Annotate) SetOSVersion(hash v1.Hash, osVersion string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Platform.OSVersion = osVersion + a.Instance[hash] = desc +} + +// Returns `Features` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) Features(hash v1.Hash) (features []string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil || len(desc.Platform.Features) == 0 { + return features, ErrFeaturesUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.Platform.Features, nil +} + +// Annotates the `Features` of the given Image. +func (a *Annotate) SetFeatures(hash v1.Hash, features []string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Platform.Features = features + a.Instance[hash] = desc +} + +// Returns `OSFeatures` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) OSFeatures(hash v1.Hash) (osFeatures []string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil || len(desc.Platform.OSFeatures) == 0 { + return osFeatures, ErrOSFeaturesUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.Platform.OSFeatures, nil +} + +// Annotates the `OSFeatures` of the given Image. +func (a *Annotate) SetOSFeatures(hash v1.Hash, osFeatures []string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Platform.OSFeatures = osFeatures + a.Instance[hash] = desc +} + +// Returns `Annotations` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) Annotations(hash v1.Hash) (annotations map[string]string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if len(desc.Annotations) == 0 { + return annotations, ErrAnnotationsUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.Annotations, nil +} + +// Annotates the `Annotations` of the given Image. +func (a *Annotate) SetAnnotations(hash v1.Hash, annotations map[string]string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.Annotations = annotations + a.Instance[hash] = desc +} + +// Returns `URLs` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) URLs(hash v1.Hash) (urls []string, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if len(desc.URLs) == 0 { + return urls, ErrURLsUndefined(types.DockerConfigJSON, hash.String()) + } + + return desc.URLs, nil +} + +// Annotates the `URLs` of the given Image. +func (a *Annotate) SetURLs(hash v1.Hash, urls []string) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.URLs = urls + a.Instance[hash] = desc +} + +// Returns `types.MediaType` of an existing manipulated ImageIndex if found, else an error. +func (a *Annotate) Format(hash v1.Hash) (format types.MediaType, err error) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.MediaType == types.MediaType("") { + return format, ErrUnknownMediaType(desc.MediaType) + } + + return desc.MediaType, nil +} + +// Stores the `Format` of the given Image. +func (a *Annotate) SetFormat(hash v1.Hash, format types.MediaType) { + if len(a.Instance) == 0 { + a.Instance = make(map[v1.Hash]v1.Descriptor) + } + + desc := a.Instance[hash] + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + desc.MediaType = format + a.Instance[hash] = desc +} + +func updatePlatform(config *v1.ConfigFile, platform *v1.Platform) error { + if config == nil { + return ErrConfigFileUndefined + } + + if platform == nil { + return ErrPlatformUndefined + } + + if platform.OS == "" { + platform.OS = config.OS + } + + if platform.Architecture == "" { + platform.Architecture = config.Architecture + } + + if platform.Variant == "" { + platform.Variant = config.Variant + } + + if platform.OSVersion == "" { + platform.OSVersion = config.OSVersion + } + + if len(platform.Features) == 0 { + p := config.Platform() + if p == nil { + p = &v1.Platform{} + } + + platform.Features = p.Features + } + + if len(platform.OSFeatures) == 0 { + platform.OSFeatures = config.OSFeatures + } + + return nil +} + +// Annotate and Append Manifests to ImageIndex. +func appendAnnotatedManifests(desc v1.Descriptor, imgDesc v1.Descriptor, path layout.Path, errs *SaveError) { + if len(desc.Annotations) != 0 && (imgDesc.MediaType == types.OCIImageIndex || imgDesc.MediaType == types.OCIManifestSchema1) { + if len(imgDesc.Annotations) == 0 { + imgDesc.Annotations = make(map[string]string, 0) + } + + for k, v := range desc.Annotations { + imgDesc.Annotations[k] = v + } + } + + if len(desc.URLs) != 0 { + imgDesc.URLs = append(imgDesc.URLs, desc.URLs...) + } + + if p := desc.Platform; p != nil { + if imgDesc.Platform == nil { + imgDesc.Platform = &v1.Platform{} + } + + if p.OS != "" { + imgDesc.Platform.OS = p.OS + } + + if p.Architecture != "" { + imgDesc.Platform.Architecture = p.Architecture + } + + if p.Variant != "" { + imgDesc.Platform.Variant = p.Variant + } + + if p.OSVersion != "" { + imgDesc.Platform.OSVersion = p.OSVersion + } + + if len(p.Features) != 0 { + imgDesc.Platform.Features = append(imgDesc.Platform.Features, p.Features...) + } + + if len(p.OSFeatures) != 0 { + imgDesc.Platform.OSFeatures = append(imgDesc.Platform.OSFeatures, p.OSFeatures...) + } + } + + path.RemoveDescriptors(match.Digests(imgDesc.Digest)) + if err := path.AppendDescriptor(imgDesc); err != nil { + errs.Errors = append(errs.Errors, SaveDiagnostic{ + Cause: err, + }) + } +} + +func parseReferenceToHash(ref name.Reference, options IndexOptions) (hash v1.Hash, err error) { + switch v := ref.(type) { + case name.Tag: + desc, err := remote.Head( + v, + remote.WithAuthFromKeychain(options.KeyChain), + remote.WithTransport( + GetTransport(options.InsecureRegistry), + ), + ) + if err != nil { + return hash, err + } + + if desc == nil { + return hash, ErrManifestUndefined + } + + hash = desc.Digest + default: + hash, err = v1.NewHash(v.Identifier()) + if err != nil { + return hash, err + } + } + + return hash, nil +} + +func getIndexManifest(ii v1.ImageIndex) (mfest *v1.IndexManifest, err error) { + mfest, err = ii.IndexManifest() + if mfest == nil { + return mfest, ErrManifestUndefined + } + + return mfest, err +} + +func indexMediaType(format types.MediaType) string { + switch format { + case types.DockerManifestList, types.DockerManifestSchema2: + return "Docker" + case types.OCIImageIndex, types.OCIManifestSchema1: + return "OCI" + default: + return "UNKNOWN" + } +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 00000000..d5b108ff --- /dev/null +++ b/util_test.go @@ -0,0 +1,378 @@ +package imgutil_test + +import ( + "encoding/json" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/fakes" + h "github.com/buildpacks/imgutil/testhelpers" +) + +func TestUtils(t *testing.T) { + spec.Run(t, "Utils", testUtils, spec.Sequential(), spec.Report(report.Terminal{})) +} + +type FakeIndentifier struct { + hash string +} + +func NewFakeIdentifier(hash string) FakeIndentifier { + return FakeIndentifier{ + hash: hash, + } +} + +func (f FakeIndentifier) String() string { + return f.hash +} + +func testUtils(t *testing.T, when spec.G, it spec.S) { + const fakeHash = "sha256:13553267bf712ee37527bdbbde41115b287062b72e2d54c573edf68d88e3cb4f" + when("#MutateManifest", func() { + var ( + img *fakes.Image + ) + it.Before(func() { + img = fakes.NewImage("some-name", fakeHash, NewFakeIdentifier(fakeHash)) + }) + it("should muatet Image", func() { + var ( + annotations = map[string]string{"some-key": "some-value"} + urls = []string{"some-url1", "some-url2"} + os = "some-os" + arch = "some-arch" + variant = "some-variant" + osVersion = "some-os-version" + features = []string{"some-feat1", "some-feat2"} + osFeatures = []string{"some-os-feat1", "some-os-feat2"} + ) + + exptConfig, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotNil(t, exptConfig) + + img, err := imgutil.MutateManifest(img, func(c *v1.Manifest) { + c.Annotations = annotations + c.Config.URLs = urls + c.Config.Platform.OS = os + c.Config.Platform.Architecture = arch + c.Config.Platform.Variant = variant + c.Config.Platform.OSVersion = osVersion + c.Config.Platform.Features = features + c.Config.Platform.OSFeatures = osFeatures + }) + + h.AssertNil(t, err) + mfest, err := img.Manifest() + h.AssertNil(t, err) + h.AssertNotNil(t, mfest) + + h.AssertEq(t, mfest.Annotations, annotations) + h.AssertEq(t, mfest.Subject.URLs, urls) + h.AssertEq(t, mfest.Subject.Platform.OS, os) + h.AssertEq(t, mfest.Subject.Platform.Architecture, arch) + h.AssertEq(t, mfest.Subject.Platform.Variant, variant) + h.AssertEq(t, mfest.Subject.Platform.OSVersion, osVersion) + h.AssertEq(t, mfest.Subject.Platform.Features, features) + h.AssertEq(t, mfest.Subject.Platform.OSFeatures, osFeatures) + + orgConfig, err := img.ConfigFile() + h.AssertNil(t, err) + h.AssertNotNil(t, orgConfig) + + h.AssertEq(t, orgConfig, exptConfig) + }) + }) + when("#TaggableIndex", func() { + var ( + taggableIndex *imgutil.TaggableIndex + amd64Hash, _ = v1.NewHash("sha256:b9d056b83bb6446fee29e89a7fcf10203c562c1f59586a6e2f39c903597bda34") + armv6Hash, _ = v1.NewHash("sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a") + indexManifest = v1.IndexManifest{ + SchemaVersion: 2, + MediaType: types.OCIImageIndex, + Annotations: map[string]string{ + "test-key": "test-value", + }, + Manifests: []v1.Descriptor{ + { + MediaType: types.OCIManifestSchema1, + Size: 832, + Digest: amd64Hash, + Platform: &v1.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + { + MediaType: types.OCIManifestSchema1, + Size: 926, + Digest: armv6Hash, + Platform: &v1.Platform{ + OS: "linux", + Architecture: "arm", + OSVersion: "v6", + }, + }, + }, + } + ) + it.Before(func() { + taggableIndex = imgutil.NewTaggableIndex(&indexManifest) + }) + it("should return RawManifest in expected format", func() { + mfestBytes, err := taggableIndex.RawManifest() + h.AssertNil(t, err) + + expectedMfestBytes, err := json.Marshal(indexManifest) + h.AssertNil(t, err) + + h.AssertEq(t, mfestBytes, expectedMfestBytes) + }) + it("should return expected digest", func() { + digest, err := taggableIndex.Digest() + h.AssertNil(t, err) + h.AssertEq(t, digest.String(), "sha256:2375c0dfd06dd51b313fd97df5ecf3b175380e895287dd9eb2240b13eb0b5703") + }) + it("should return expected size", func() { + size, err := taggableIndex.Size() + h.AssertNil(t, err) + h.AssertEq(t, size, int64(547)) + }) + it("should return expected media type", func() { + format, err := taggableIndex.MediaType() + h.AssertNil(t, err) + h.AssertEq(t, format, indexManifest.MediaType) + }) + }) + when("#StringSet", func() { + var ( + stringSet *imgutil.StringSet + ) + it.Before(func() { + stringSet = imgutil.NewStringSet() + }) + it("should add items", func() { + item := "item1" + stringSet.Add(item) + + h.AssertEq(t, stringSet.StringSlice(), []string{item}) + }) + it("should remove item", func() { + item := "item1" + stringSet.Add(item) + + h.AssertEq(t, stringSet.StringSlice(), []string{item}) + + stringSet.Remove(item) + h.AssertEq(t, stringSet.StringSlice(), []string(nil)) + }) + it("should return added items", func() { + items := []string{"item1", "item2", "item3"} + for _, item := range items { + stringSet.Add(item) + } + + h.AssertEq(t, stringSet.StringSlice(), items) + }) + it("should not support duplicates", func() { + stringSet := imgutil.NewStringSet() + item1 := "item1" + item2 := "item2" + items := []string{item1, item2, item1} + for _, item := range items { + stringSet.Add(item) + } + + h.AssertEq(t, stringSet.StringSlice(), []string{item1, item2}) + }) + }) + when("Annotate", func() { + annotate := imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{}, + } + it.Before(func() { + annotate = imgutil.Annotate{ + Instance: map[v1.Hash]v1.Descriptor{}, + } + }) + when("#OS", func() { + it.Before(func() { + annotate.SetOS(v1.Hash{}, "some-os") + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetOS(v1.Hash{}, "") + os, err := annotate.OS(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, os, "") + }) + it("should return expected os", func() { + os, err := annotate.OS(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + }) + }) + when("#Architecture", func() { + it.Before(func() { + annotate.SetArchitecture(v1.Hash{}, "some-arch") + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetArchitecture(v1.Hash{}, "") + arch, err := annotate.Architecture(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, arch, "") + }) + it("should return expected os", func() { + arch, err := annotate.Architecture(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, arch, "some-arch") + }) + }) + when("#Variant", func() { + it.Before(func() { + annotate.SetVariant(v1.Hash{}, "some-variant") + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetVariant(v1.Hash{}, "") + variant, err := annotate.Variant(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, variant, "") + }) + it("should return expected os", func() { + variant, err := annotate.Variant(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, variant, "some-variant") + }) + }) + when("#OSVersion", func() { + it.Before(func() { + annotate.SetOSVersion(v1.Hash{}, "some-osVersion") + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetOSVersion(v1.Hash{}, "") + osVersion, err := annotate.OSVersion(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, osVersion, "") + }) + it("should return expected os", func() { + osVersion, err := annotate.OSVersion(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, osVersion, "some-osVersion") + }) + }) + when("#Features", func() { + it.Before(func() { + annotate.SetFeatures(v1.Hash{}, []string{"some-features"}) + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetFeatures(v1.Hash{}, []string(nil)) + features, err := annotate.Features(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, features, []string(nil)) + }) + it("should return expected features", func() { + os, err := annotate.Features(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, os, []string{"some-features"}) + }) + }) + when("#OSFeatures", func() { + it.Before(func() { + annotate.SetOSFeatures(v1.Hash{}, []string{"some-osFeatures"}) + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetOSFeatures(v1.Hash{}, []string(nil)) + osFeatures, err := annotate.OSFeatures(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, osFeatures, []string(nil)) + }) + it("should return expected os", func() { + osFeatures, err := annotate.OSFeatures(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-osFeatures"}) + }) + }) + when("#Annotations", func() { + it.Before(func() { + annotate.SetAnnotations(v1.Hash{}, map[string]string{"some-key": "some-value"}) + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetAnnotations(v1.Hash{}, map[string]string(nil)) + annotations, err := annotate.Annotations(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, annotations, map[string]string(nil)) + }) + it("should return expected os", func() { + annotations, err := annotate.Annotations(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, annotations, map[string]string{"some-key": "some-value"}) + }) + }) + when("#URLs", func() { + it.Before(func() { + annotate.SetURLs(v1.Hash{}, []string{"some-urls"}) + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + }) + it("should return an error", func() { + annotate.SetURLs(v1.Hash{}, []string(nil)) + urls, err := annotate.URLs(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, urls, []string(nil)) + }) + it("should return expected os", func() { + os, err := annotate.URLs(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, os, []string{"some-urls"}) + }) + }) + when("#Format", func() { + it.Before(func() { + annotate.SetFormat(v1.Hash{}, types.OCIImageIndex) + desc, ok := annotate.Instance[v1.Hash{}] + h.AssertEq(t, ok, true) + h.AssertNotEq(t, desc, nil) + h.AssertEq(t, desc.MediaType, types.OCIImageIndex) + }) + it("should return an error", func() { + annotate.SetFormat(v1.Hash{}, types.MediaType("")) + format, err := annotate.Format(v1.Hash{}) + h.AssertNotEq(t, err, nil) + h.AssertEq(t, format, types.MediaType("")) + }) + it("should return expected os", func() { + format, err := annotate.Format(v1.Hash{}) + h.AssertNil(t, err) + h.AssertEq(t, format, types.OCIImageIndex) + }) + }) + }) +}