diff --git a/cnb_index.go b/cnb_index.go index 5e795b69..cae1b22c 100644 --- a/cnb_index.go +++ b/cnb_index.go @@ -35,161 +35,144 @@ type CNBIndex struct { RepoName string } -func (h *CNBIndex) getConfigFileFrom(digest name.Digest) (v1.ConfigFile, error) { - hash, err := v1.NewHash(digest.Identifier()) - if err != nil { - return v1.ConfigFile{}, err - } - image, err := h.Image(hash) - if err != nil { - return v1.ConfigFile{}, err - } - configFile, err := GetConfigFile(image) - if err != nil { - return v1.ConfigFile{}, err - } - return *configFile, nil -} - -func (h *CNBIndex) getManifestFileFrom(digest name.Digest) (v1.Manifest, error) { - hash, err := v1.NewHash(digest.Identifier()) - if err != nil { - return v1.Manifest{}, err - } - image, err := h.Image(hash) +func (h *CNBIndex) getDescriptorFrom(digest name.Digest) (v1.Descriptor, error) { + indexManifest, err := getIndexManifest(h.ImageIndex) if err != nil { - return v1.Manifest{}, err + return v1.Descriptor{}, err } - manifestFile, err := GetManifest(image) - if err != nil { - return v1.Manifest{}, err + for _, current := range indexManifest.Manifests { + if current.Digest.String() == digest.Identifier() { + return current, nil + } } - return *manifestFile, nil + return v1.Descriptor{}, fmt.Errorf("failed to find image with digest %s in index", digest.Identifier()) } // OS returns `OS` of an existing Image. func (h *CNBIndex) OS(digest name.Digest) (os string, err error) { - configFile, err := h.getConfigFileFrom(digest) + desc, err := h.getDescriptorFrom(digest) if err != nil { return "", err } - return configFile.OS, nil + if desc.Platform != nil { + return desc.Platform.OS, nil + } + return "", nil } // Architecture return the Architecture of an Image/Index based on given Digest. // Returns an error if no Image/Index found with given Digest. func (h *CNBIndex) Architecture(digest name.Digest) (arch string, err error) { - configFile, err := h.getConfigFileFrom(digest) + desc, err := h.getDescriptorFrom(digest) if err != nil { return "", err } - return configFile.Architecture, nil + if desc.Platform != nil { + return desc.Platform.Architecture, nil + } + return "", nil } // Variant return the `Variant` of an Image. // Returns an error if no Image/Index found with given Digest. func (h *CNBIndex) Variant(digest name.Digest) (osVariant string, err error) { - configFile, err := h.getConfigFileFrom(digest) + desc, err := h.getDescriptorFrom(digest) if err != nil { return "", err } - return configFile.Variant, nil + if desc.Platform != nil { + return desc.Platform.Variant, nil + } + return "", nil } // OSVersion returns the `OSVersion` of an Image with given Digest. // Returns an error if no Image/Index found with given Digest. func (h *CNBIndex) OSVersion(digest name.Digest) (osVersion string, err error) { - configFile, err := h.getConfigFileFrom(digest) + desc, err := h.getDescriptorFrom(digest) if err != nil { return "", err } - return configFile.OSVersion, nil + if desc.Platform != nil { + return desc.Platform.OSVersion, nil + } + return "", nil } // OSFeatures returns the `OSFeatures` of an Image with given Digest. // Returns an error if no Image/Index found with given Digest. func (h *CNBIndex) OSFeatures(digest name.Digest) (osFeatures []string, err error) { - configFile, err := h.getConfigFileFrom(digest) + desc, err := h.getDescriptorFrom(digest) if err != nil { return nil, err } - return configFile.OSFeatures, nil + if desc.Platform != nil { + return desc.Platform.OSFeatures, nil + } + return []string{}, nil } // Annotations 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 *CNBIndex) Annotations(digest name.Digest) (annotations map[string]string, err error) { - manifestFile, err := h.getManifestFileFrom(digest) + desc, err := h.getDescriptorFrom(digest) if err != nil { return nil, err } - return manifestFile.Annotations, nil + return desc.Annotations, nil } // setters func (h *CNBIndex) SetAnnotations(digest name.Digest, annotations map[string]string) (err error) { - return h.mutateExistingImage(digest, func(image v1.Image) (v1.Image, error) { - partial := mutate.Annotations(image, annotations) - annotatedImage, ok := partial.(v1.Image) - if !ok { - return nil, fmt.Errorf("failed to annotate image") + return h.replaceDescriptor(digest, func(descriptor v1.Descriptor) (v1.Descriptor, error) { + if len(descriptor.Annotations) == 0 { + descriptor.Annotations = make(map[string]string) } - return annotatedImage, nil + + for k, v := range annotations { + descriptor.Annotations[k] = v + } + return descriptor, nil }) } func (h *CNBIndex) SetArchitecture(digest name.Digest, arch string) (err error) { - return h.mutateExistingImage(digest, func(image v1.Image) (v1.Image, error) { - configFile, err := image.ConfigFile() - if err != nil { - return nil, err - } - configFile.Architecture = arch - return mutate.ConfigFile(image, configFile) + return h.replaceDescriptor(digest, func(descriptor v1.Descriptor) (v1.Descriptor, error) { + descriptor.Platform.Architecture = arch + return descriptor, nil }) } func (h *CNBIndex) SetOS(digest name.Digest, os string) (err error) { - return h.mutateExistingImage(digest, func(image v1.Image) (v1.Image, error) { - configFile, err := image.ConfigFile() - if err != nil { - return nil, err - } - configFile.OS = os - return mutate.ConfigFile(image, configFile) + return h.replaceDescriptor(digest, func(descriptor v1.Descriptor) (v1.Descriptor, error) { + descriptor.Platform.OS = os + return descriptor, nil }) } func (h *CNBIndex) SetVariant(digest name.Digest, osVariant string) (err error) { - return h.mutateExistingImage(digest, func(image v1.Image) (v1.Image, error) { - configFile, err := image.ConfigFile() - if err != nil { - return nil, err - } - configFile.Variant = osVariant - return mutate.ConfigFile(image, configFile) + return h.replaceDescriptor(digest, func(descriptor v1.Descriptor) (v1.Descriptor, error) { + descriptor.Platform.Variant = osVariant + return descriptor, nil }) } -func (h *CNBIndex) mutateExistingImage(digest name.Digest, withFunc func(image v1.Image) (v1.Image, error)) (err error) { - hash, err := v1.NewHash(digest.Identifier()) +func (h *CNBIndex) replaceDescriptor(digest name.Digest, withFun func(descriptor v1.Descriptor) (v1.Descriptor, error)) (err error) { + desc, err := h.getDescriptorFrom(digest) if err != nil { return err } - image, err := h.Image(hash) + desc, err = withFun(desc) if err != nil { return err } - if err = h.RemoveManifest(digest); err != nil { - return err - } - newImage, err := withFunc(image) - if err != nil { - return err + add := mutate.IndexAddendum{ + Add: h.ImageIndex, + Descriptor: desc, } - h.AddManifest(newImage) + h.ImageIndex = mutate.AppendManifests(mutate.RemoveManifests(h.ImageIndex, match.Digests(desc.Digest)), add) return nil } @@ -215,8 +198,10 @@ func indexContains(manifests []v1.Descriptor, hash v1.Hash) bool { // AddManifest adds an image to the index. func (h *CNBIndex) AddManifest(image v1.Image) { + desc, _ := descriptor(image) h.ImageIndex = mutate.AppendManifests(h.ImageIndex, mutate.IndexAddendum{ - Add: image, + Add: image, + Descriptor: desc, }) } @@ -378,3 +363,19 @@ func getIndexManifest(ii v1.ImageIndex) (mfest *v1.IndexManifest, err error) { } return mfest, err } + +// descriptor returns a v1.Descriptor filled with a v1.Platform created from reading +// the image config file. +func descriptor(image v1.Image) (v1.Descriptor, error) { + // Get the image configuration file + cfg, _ := GetConfigFile(image) + platform := v1.Platform{} + platform.Architecture = cfg.Architecture + platform.OS = cfg.OS + platform.OSVersion = cfg.OSVersion + platform.Variant = cfg.Variant + platform.OSFeatures = cfg.OSFeatures + return v1.Descriptor{ + Platform: &platform, + }, nil +} diff --git a/layout/layout_test.go b/layout/layout_test.go index 4f21b18e..021fbeb6 100644 --- a/layout/layout_test.go +++ b/layout/layout_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "strings" "testing" "time" @@ -1267,6 +1268,112 @@ func testIndex(t *testing.T, when spec.G, it spec.S) { }) }) + when("#Setters", func() { + var ( + descriptor1 v1.Descriptor + digest1 name.Digest + ) + + when("index is created from scratch", func() { + it.Before(func() { + repoName := newRepoName() + idx = setupIndex(t, repoName, imgutil.WithXDGRuntimePath(tmpDir)) + localPath = filepath.Join(tmpDir, repoName) + }) + + when("digest is provided", func() { + it.Before(func() { + image1, err := random.Image(1024, 1) + h.AssertNil(t, err) + idx.AddManifest(image1) + + h.AssertNil(t, idx.SaveDir()) + + index := h.ReadIndexManifest(t, localPath) + h.AssertEq(t, len(index.Manifests), 1) + descriptor1 = index.Manifests[0] + + digest1, err = name.NewDigest(fmt.Sprintf("%s@%s", "random", descriptor1.Digest.String())) + h.AssertNil(t, err) + }) + + it("platform attributes are written on disk", func() { + h.AssertNil(t, idx.SetOS(digest1, "linux")) + h.AssertNil(t, idx.SetArchitecture(digest1, "arm")) + h.AssertNil(t, idx.SetVariant(digest1, "v6")) + h.AssertNil(t, idx.SaveDir()) + + index := h.ReadIndexManifest(t, localPath) + h.AssertEq(t, len(index.Manifests), 1) + h.AssertEq(t, index.Manifests[0].Digest.String(), descriptor1.Digest.String()) + h.AssertEq(t, index.Manifests[0].Platform.OS, "linux") + h.AssertEq(t, index.Manifests[0].Platform.Architecture, "arm") + h.AssertEq(t, index.Manifests[0].Platform.Variant, "v6") + }) + + it("annotations are written on disk", func() { + annotations := map[string]string{ + "some-key": "some-value", + } + h.AssertNil(t, idx.SetAnnotations(digest1, annotations)) + h.AssertNil(t, idx.SaveDir()) + + index := h.ReadIndexManifest(t, localPath) + h.AssertEq(t, len(index.Manifests), 1) + h.AssertEq(t, index.Manifests[0].Digest.String(), descriptor1.Digest.String()) + h.AssertEq(t, reflect.DeepEqual(index.Manifests[0].Annotations, annotations), true) + }) + }) + }) + + when("index exists on disk", func() { + when("#FromBaseIndex", func() { + it.Before(func() { + idx = setupIndex(t, "busybox-multi-platform", imgutil.WithXDGRuntimePath(tmpDir), imgutil.FromBaseIndex(baseIndexPath)) + localPath = filepath.Join(tmpDir, "busybox-multi-platform") + digest1, err = name.NewDigest("busybox@sha256:e18f2c12bb4ea582045415243370a3d9cf3874265aa2867f21a35e630ebe45a7") + h.AssertNil(t, err) + }) + + when("digest is provided", func() { + when("attributes already exists", func() { + it("platform attributes are updated on disk", func() { + h.AssertNil(t, idx.SetOS(digest1, "linux-2")) + h.AssertNil(t, idx.SetArchitecture(digest1, "arm-2")) + h.AssertNil(t, idx.SetVariant(digest1, "v6-2")) + h.AssertNil(t, idx.SaveDir()) + + index := h.ReadIndexManifest(t, localPath) + h.AssertEq(t, len(index.Manifests), 2) + h.AssertEq(t, index.Manifests[1].Digest.String(), "sha256:e18f2c12bb4ea582045415243370a3d9cf3874265aa2867f21a35e630ebe45a7") + h.AssertEq(t, index.Manifests[1].Platform.OS, "linux-2") + h.AssertEq(t, index.Manifests[1].Platform.Architecture, "arm-2") + h.AssertEq(t, index.Manifests[1].Platform.Variant, "v6-2") + }) + + it("new annotation are appended on disk", func() { + annotations := map[string]string{ + "some-key": "some-value", + } + h.AssertNil(t, idx.SetAnnotations(digest1, annotations)) + h.AssertNil(t, idx.SaveDir()) + + index := h.ReadIndexManifest(t, localPath) + h.AssertEq(t, len(index.Manifests), 2) + + // When updating a digest, it will be appended at the end + h.AssertEq(t, index.Manifests[1].Digest.String(), "sha256:e18f2c12bb4ea582045415243370a3d9cf3874265aa2867f21a35e630ebe45a7") + + // in testdata we have 7 annotations + 1 new + h.AssertEq(t, len(index.Manifests[1].Annotations), 8) + h.AssertEq(t, index.Manifests[1].Annotations["some-key"], "some-value") + }) + }) + }) + }) + }) + }) + when("#Save", func() { when("index exists on disk", func() { when("#FromBaseIndex", func() { @@ -1348,7 +1455,7 @@ func testIndex(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, len(index.Manifests), 1) }) - it.Focus("add more than one manifest to the index", func() { + it("add more than one manifest to the index", func() { image1, err := random.Image(1024, 1) h.AssertNil(t, err) idx.AddManifest(image1) diff --git a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/451a150a3f3f33286ab635e81a2d321a5ad7e7b1d4618c9aa705e69780a412b1 b/layout/testdata/layout/busybox-multi-platform/blobs/sha256/451a150a3f3f33286ab635e81a2d321a5ad7e7b1d4618c9aa705e69780a412b1 deleted file mode 100644 index 971c88f9..00000000 --- a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/451a150a3f3f33286ab635e81a2d321a5ad7e7b1d4618c9aa705e69780a412b1 +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "Cmd": [ - "sh" - ] - }, - "created": "2023-05-18T22:34:17Z", - "history": [ - { - "created": "2023-05-18T22:34:17Z", - "created_by": "BusyBox 1.36.1 (glibc), Debian 12" - } - ], - "rootfs": { - "type": "layers", - "diff_ids": [ - "sha256:f102f3de6a6160248ca4e9d65042808dc19470f59472115d6964724217978bf1" - ] - }, - "architecture": "arm", - "os": "linux", - "variant": "v7", - "os.features": ["os-feature-3", "os-feature-4"], - "os.version": "1.2.3" -} diff --git a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/94fe54b22e44477ef2b7ce8f38a506c9e61b91053f4f0607dda9458b3972ecc1 b/layout/testdata/layout/busybox-multi-platform/blobs/sha256/94fe54b22e44477ef2b7ce8f38a506c9e61b91053f4f0607dda9458b3972ecc1 deleted file mode 100644 index 3aec2685..00000000 --- a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/94fe54b22e44477ef2b7ce8f38a506c9e61b91053f4f0607dda9458b3972ecc1 +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "Cmd": [ - "sh" - ] - }, - "created": "2023-05-18T22:34:17Z", - "history": [ - { - "created": "2023-05-18T22:34:17Z", - "created_by": "BusyBox 1.36.1 (glibc), Debian 12" - } - ], - "rootfs": { - "type": "layers", - "diff_ids": [ - "sha256:95c4a60383f7b6eb6f7b8e153a07cd6e896de0476763bef39d0f6cf3400624bd" - ] - }, - "architecture": "amd64", - "os": "linux", - "variant": "v1", - "os.features": ["os-feature-1", "os-feature-2"], - "os.version": "4.5.6" -} diff --git a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/e18f2c12bb4ea582045415243370a3d9cf3874265aa2867f21a35e630ebe45a7 b/layout/testdata/layout/busybox-multi-platform/blobs/sha256/e18f2c12bb4ea582045415243370a3d9cf3874265aa2867f21a35e630ebe45a7 deleted file mode 100644 index 058ab552..00000000 --- a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/e18f2c12bb4ea582045415243370a3d9cf3874265aa2867f21a35e630ebe45a7 +++ /dev/null @@ -1,21 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.image.config.v1+json", - "digest": "sha256:451a150a3f3f33286ab635e81a2d321a5ad7e7b1d4618c9aa705e69780a412b1", - "size": 388 - }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:a7cbd68a76a020b8b283c940bc267cd88a66013dcb160cad746344483dfc4b52", - "size": 1554425 - } - ], - "annotations": { - "org.opencontainers.image.url": "https://hub.docker.com/_/busybox", - "com.docker.official-images.bashbrew.arch": "arm32v7", - "org.opencontainers.image.revision": "185a3f7f21c307b15ef99b7088b228f004ff5f11" - } -} diff --git a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/f5b920213fc6498c0c5eaee7e04f8424202b565bb9e5e4de9e617719fb7bd873 b/layout/testdata/layout/busybox-multi-platform/blobs/sha256/f5b920213fc6498c0c5eaee7e04f8424202b565bb9e5e4de9e617719fb7bd873 deleted file mode 100644 index 7e9ddae4..00000000 --- a/layout/testdata/layout/busybox-multi-platform/blobs/sha256/f5b920213fc6498c0c5eaee7e04f8424202b565bb9e5e4de9e617719fb7bd873 +++ /dev/null @@ -1,21 +0,0 @@ -{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "config": { - "mediaType": "application/vnd.oci.image.config.v1+json", - "digest": "sha256:94fe54b22e44477ef2b7ce8f38a506c9e61b91053f4f0607dda9458b3972ecc1", - "size": 372 - }, - "layers": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:7b2699543f22d5b8dc8d66a5873eb246767bca37232dee1e7a3b8c9956bceb0c", - "size": 2152262 - } - ], - "annotations": { - "org.opencontainers.image.url": "https://hub.docker.com/_/busybox", - "com.docker.official-images.bashbrew.arch": "amd64", - "org.opencontainers.image.revision": "d0b7d566eb4f1fa9933984e6fc04ab11f08f4592" - } -} diff --git a/util.go b/util.go index 01b62e36..0dcde65f 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,6 @@ package imgutil import ( "encoding/json" - "slices" "strings" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -108,25 +107,6 @@ func (s *StringSet) StringSlice() (slice []string) { return slice } -func MapContains(src, target map[string]string) bool { - for targetKey, targetValue := range target { - if value := src[targetKey]; targetValue == value { - continue - } - return false - } - return true -} - -func SliceContains(src, target []string) bool { - for _, value := range target { - if ok := slices.Contains[[]string, string](src, value); !ok { - return false - } - } - return true -} - // MakeFileSafeName Change a reference name string into a valid file name // Ex: cnbs/sample-package:hello-multiarch-universe // to cnbs_sample-package-hello-multiarch-universe