From dbfef49219d8926e5a8a3952d42b371c31313c25 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 19 Aug 2024 04:59:53 -0600 Subject: [PATCH 01/30] refactor: remove build constraint from testutils console (#1480) Signed-off-by: Terry Howe --- internal/testutils/console.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/testutils/console.go b/internal/testutils/console.go index 648c685b2..8a262340b 100644 --- a/internal/testutils/console.go +++ b/internal/testutils/console.go @@ -1,5 +1,3 @@ -//go:build freebsd || linux || netbsd || openbsd || solaris - /* Copyright The ORAS Authors. Licensed under the Apache License, Version 2.0 (the "License"); From d4eccf467bd50e5514d79f1290a45ecbfa09d7de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 04:11:42 -0600 Subject: [PATCH 02/30] build(deps): bump github.com/onsi/ginkgo/v2 from 2.20.0 to 2.20.1 in /test/e2e (#1486) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/e2e/go.mod | 2 +- test/e2e/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 427ec9c5e..e7b96aeef 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -3,7 +3,7 @@ module oras.land/oras/test/e2e go 1.22.0 require ( - github.com/onsi/ginkgo/v2 v2.20.0 + github.com/onsi/ginkgo/v2 v2.20.1 github.com/onsi/gomega v1.34.1 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 1933b78ba..a63d4a92d 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -8,8 +8,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= -github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= -github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= From 5792c35622a75a87d8f7e29f0a8b4e33105a13df Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 22 Aug 2024 04:41:39 -0600 Subject: [PATCH 03/30] fix: copy prompt skipped was missing (#1484) Signed-off-by: Terry Howe Signed-off-by: Billy Zha Co-authored-by: Billy Zha --- cmd/oras/internal/display/status/text.go | 9 +++++---- cmd/oras/internal/display/status/text_test.go | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/cmd/oras/internal/display/status/text.go b/cmd/oras/internal/display/status/text.go index 2ac3a6235..c4277199f 100644 --- a/cmd/oras/internal/display/status/text.go +++ b/cmd/oras/internal/display/status/text.go @@ -17,9 +17,10 @@ package status import ( "context" - "oras.land/oras/internal/graph" "sync" + "oras.land/oras/internal/graph" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" @@ -161,12 +162,12 @@ func (ch *TextCopyHandler) PreCopy(_ context.Context, desc ocispec.Descriptor) e // PostCopy implements PostCopy of CopyHandler. func (ch *TextCopyHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - successors, err := graph.FilteredSuccessors(ctx, desc, ch.fetcher, DeduplicatedFilter(ch.committed)) + deduplicated, err := graph.FilteredSuccessors(ctx, desc, ch.fetcher, DeduplicatedFilter(ch.committed)) if err != nil { return err } - for _, successor := range successors { - if err = ch.printer.PrintStatus(successor, copyPromptExists); err != nil { + for _, successor := range deduplicated { + if err = ch.printer.PrintStatus(successor, copyPromptSkipped); err != nil { return err } } diff --git a/cmd/oras/internal/display/status/text_test.go b/cmd/oras/internal/display/status/text_test.go index 6a1d7c75d..8e68aa76b 100644 --- a/cmd/oras/internal/display/status/text_test.go +++ b/cmd/oras/internal/display/status/text_test.go @@ -19,6 +19,7 @@ import ( "context" "os" "strings" + "sync" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -68,7 +69,7 @@ func TestTextCopyHandler_OnCopySkipped(t *testing.T) { validatePrinted(t, "Exists 0b442c23c1dd oci-image") } -func TestTextCopyHandler_PostCopy(t *testing.T) { +func TestTextCopyHandler_PostCopy_titled(t *testing.T) { builder.Reset() ch := NewTextCopyHandler(printer, mockFetcher.Fetcher) if ch.PostCopy(ctx, mockFetcher.OciImage) != nil { @@ -80,6 +81,20 @@ func TestTextCopyHandler_PostCopy(t *testing.T) { validatePrinted(t, "Copied 0b442c23c1dd oci-image") } +func TestTextCopyHandler_PostCopy_skipped(t *testing.T) { + builder.Reset() + ch := &TextCopyHandler{ + printer: printer, + fetcher: mockFetcher.Fetcher, + committed: &sync.Map{}, + } + ch.committed.Store(mockFetcher.ImageLayer.Digest.String(), mockFetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]+"bogus") + if err := ch.PostCopy(ctx, mockFetcher.OciImage); err != nil { + t.Error("PostCopy() returns unexpected err:", err) + } + validatePrinted(t, "Skipped f6b87e8e0fe1 layer\nCopied 0b442c23c1dd oci-image") +} + func TestTextCopyHandler_PreCopy(t *testing.T) { builder.Reset() ch := NewTextCopyHandler(printer, mockFetcher.Fetcher) From 3fce911bc0de46d6aaa1dabe3ca5a8a21049df3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 06:58:05 +0000 Subject: [PATCH 04/30] build(deps): bump github.com/onsi/gomega from 1.34.1 to 1.34.2 in /test/e2e (#1489) --- test/e2e/go.mod | 7 +++---- test/e2e/go.sum | 14 ++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/test/e2e/go.mod b/test/e2e/go.mod index e7b96aeef..188b0aa53 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -4,7 +4,7 @@ go 1.22.0 require ( github.com/onsi/ginkgo/v2 v2.20.1 - github.com/onsi/gomega v1.34.1 + github.com/onsi/gomega v1.34.2 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 gopkg.in/yaml.v2 v2.4.0 @@ -15,11 +15,10 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/test/e2e/go.sum b/test/e2e/go.sum index a63d4a92d..c0dc05342 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -6,12 +6,12 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -20,14 +20,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= From 0013d4cb47642978e5969163283ecf5305642f64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 07:01:50 +0000 Subject: [PATCH 05/30] build(deps): bump github.com/onsi/ginkgo/v2 from 2.20.1 to 2.20.2 in /test/e2e (#1488) --- test/e2e/go.mod | 2 +- test/e2e/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 188b0aa53..ea1b056ac 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -3,7 +3,7 @@ module oras.land/oras/test/e2e go 1.22.0 require ( - github.com/onsi/ginkgo/v2 v2.20.1 + github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 diff --git a/test/e2e/go.sum b/test/e2e/go.sum index c0dc05342..5044da73f 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -8,8 +8,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= From 749d2ff233a6fbbbe1a339a9920ded2f94029882 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 07:42:59 -0600 Subject: [PATCH 06/30] build(deps): bump github.com/Masterminds/sprig/v3 from 3.2.3 to 3.3.0 (#1491) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 14 ++++++------ go.sum | 72 +++++++++++++--------------------------------------------- 2 files changed, 23 insertions(+), 63 deletions(-) diff --git a/go.mod b/go.mod index 2b8fdb23a..1e3f76227 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module oras.land/oras go 1.22.0 require ( - github.com/Masterminds/sprig/v3 v3.2.3 + github.com/Masterminds/sprig/v3 v3.3.0 github.com/containerd/console v1.0.4 github.com/morikuni/aec v1.0.0 github.com/opencontainers/go-digest v1.0.0 @@ -18,16 +18,16 @@ require ( ) require ( + dario.cat/mergo v1.0.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/huandu/xstrings v1.4.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/spf13/cast v1.6.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/sys v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index e8d5662ee..ef22a198d 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,11 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -13,27 +14,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -47,65 +41,31 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 4c88cb82d2e4c14d71c0fa5fc4a688b154dbbf0e Mon Sep 17 00:00:00 2001 From: "Lixia (Sylvia) Lei" Date: Tue, 3 Sep 2024 14:51:32 +0800 Subject: [PATCH 07/30] build: upgrade to Go `1.23.0` (#1493) Signed-off-by: Lixia (Sylvia) Lei --- .github/workflows/build.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/release-github.yml | 2 +- Dockerfile | 2 +- go.mod | 2 +- snapcraft.yaml | 2 +- test/e2e/go.mod | 2 +- test/e2e/go.work | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3daa22370..e27f9c464 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.22'] + go-version: ['1.23'] fail-fast: true steps: - name: Checkout diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9017d5280..56340b1d4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,7 +35,7 @@ jobs: security-events: write strategy: matrix: - go-version: ['1.22'] + go-version: ['1.23'] fail-fast: false steps: - name: Checkout repository diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e66f240bb..a8adf1273 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.22'] + go-version: ['1.23'] fail-fast: true steps: - name: Checkout diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml index f5c8bf46d..e1c066139 100644 --- a/.github/workflows/release-github.yml +++ b/.github/workflows/release-github.yml @@ -29,7 +29,7 @@ jobs: - name: setup go environment uses: actions/setup-go@v5 with: - go-version: '1.22.3' + go-version: '1.23.0' - name: run goreleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/Dockerfile b/Dockerfile index 91bc29572..f999e0281 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.22.3-alpine as builder +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.23.0-alpine as builder ARG TARGETPLATFORM RUN apk add git make ENV ORASPKG /oras diff --git a/go.mod b/go.mod index 1e3f76227..c6b44e79c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module oras.land/oras -go 1.22.0 +go 1.23.0 require ( github.com/Masterminds/sprig/v3 v3.3.0 diff --git a/snapcraft.yaml b/snapcraft.yaml index 8ee8cf535..8082d8403 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -34,7 +34,7 @@ parts: - on amd64 to {ARCH}: - TARGET_ARCH: "{ARCH}" build-snaps: - - go/1.22/stable + - go/1.23/stable build-packages: - make stage-packages: diff --git a/test/e2e/go.mod b/test/e2e/go.mod index ea1b056ac..71de7dd8e 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -1,6 +1,6 @@ module oras.land/oras/test/e2e -go 1.22.0 +go 1.23.0 require ( github.com/onsi/ginkgo/v2 v2.20.2 diff --git a/test/e2e/go.work b/test/e2e/go.work index 4cbad6511..30c306ef4 100644 --- a/test/e2e/go.work +++ b/test/e2e/go.work @@ -1,4 +1,4 @@ -go 1.22.0 +go 1.23.0 use ( . From 3dc7eeb947be92ac548d4e3eab477305f7e0a2d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 05:14:12 +0000 Subject: [PATCH 08/30] build(deps): bump golang.org/x/term from 0.23.0 to 0.24.0 (#1495) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c6b44e79c..46c9173ee 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 golang.org/x/sync v0.8.0 - golang.org/x/term v0.23.0 + golang.org/x/term v0.24.0 gopkg.in/yaml.v3 v3.0.1 oras.land/oras-go/v2 v2.5.0 ) @@ -29,5 +29,5 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect golang.org/x/crypto v0.26.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index ef22a198d..467cc7dfc 100644 --- a/go.sum +++ b/go.sum @@ -60,10 +60,10 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 40dd8f921cf9183743226193040946f5f42e41ea Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Fri, 6 Sep 2024 09:50:50 +0800 Subject: [PATCH 09/30] feat: create index from existing manifests (index create) (#1475) Signed-off-by: Xiaoxuan Wang --- cmd/oras/internal/display/handler.go | 5 + .../internal/display/metadata/interface.go | 5 + .../metadata/text/manifest_index_create.go | 39 +++ cmd/oras/internal/display/status/utils.go | 8 + cmd/oras/root/manifest/cmd.go | 2 + cmd/oras/root/manifest/index/cmd.go | 30 +++ cmd/oras/root/manifest/index/create.go | 181 ++++++++++++++ internal/descriptor/descriptor.go | 24 ++ internal/descriptor/descriptor_test.go | 12 + test/e2e/README.md | 7 + .../e2e/internal/testdata/multi_arch/const.go | 24 ++ .../internal/testdata/nonjson_config/const.go | 28 +++ test/e2e/scripts/prepare.sh | 2 +- test/e2e/suite/command/manifest_index.go | 228 ++++++++++++++++++ ...c716615d3c0d8beb87dc68ed88bb49192f90b184e2 | 1 + ...8abb782f69fa2b19164464b200351b3be3b690cf4a | 1 + ...cb21216704ba8c919997d0f1f37e154c11d509e1d2 | 1 + .../testdata/zot/command/images/index.json | 116 ++++----- 18 files changed, 658 insertions(+), 56 deletions(-) create mode 100644 cmd/oras/internal/display/metadata/text/manifest_index_create.go create mode 100644 cmd/oras/root/manifest/index/cmd.go create mode 100644 cmd/oras/root/manifest/index/create.go create mode 100644 test/e2e/internal/testdata/nonjson_config/const.go create mode 100644 test/e2e/suite/command/manifest_index.go create mode 100644 test/e2e/testdata/zot/command/images/blobs/sha256/02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2 create mode 100644 test/e2e/testdata/zot/command/images/blobs/sha256/24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a create mode 100644 test/e2e/testdata/zot/command/images/blobs/sha256/9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2 diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index 2f7428585..ccad542aa 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -174,6 +174,11 @@ func NewManifestPushHandler(printer *output.Printer) metadata.ManifestPushHandle return text.NewManifestPushHandler(printer) } +// NewManifestIndexCreateHandler returns an index create handler. +func NewManifestIndexCreateHandler(printer *output.Printer) metadata.ManifestIndexCreateHandler { + return text.NewManifestIndexCreateHandler(printer) +} + // NewCopyHandler returns copy handlers. func NewCopyHandler(printer *output.Printer, fetcher fetcher.Fetcher) (status.CopyHandler, metadata.CopyHandler) { return status.NewTextCopyHandler(printer, fetcher), text.NewCopyHandler(printer) diff --git a/cmd/oras/internal/display/metadata/interface.go b/cmd/oras/internal/display/metadata/interface.go index aacc91309..12c10b87b 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -78,6 +78,11 @@ type ManifestPushHandler interface { TaggedHandler } +// ManifestIndexCreateHandler handles metadata output for index create events. +type ManifestIndexCreateHandler interface { + TaggedHandler +} + // CopyHandler handles metadata output for cp events. type CopyHandler interface { TaggedHandler diff --git a/cmd/oras/internal/display/metadata/text/manifest_index_create.go b/cmd/oras/internal/display/metadata/text/manifest_index_create.go new file mode 100644 index 000000000..960f676c2 --- /dev/null +++ b/cmd/oras/internal/display/metadata/text/manifest_index_create.go @@ -0,0 +1,39 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package text + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/display/metadata" + "oras.land/oras/cmd/oras/internal/output" +) + +// ManifestIndexCreateHandler handles text metadata output for index create events. +type ManifestIndexCreateHandler struct { + printer *output.Printer +} + +// NewManifestIndexCreateHandler returns a new handler for index create events. +func NewManifestIndexCreateHandler(printer *output.Printer) metadata.ManifestIndexCreateHandler { + return &ManifestIndexCreateHandler{ + printer: printer, + } +} + +// OnTagged implements metadata.TaggedHandler. +func (h *ManifestIndexCreateHandler) OnTagged(_ ocispec.Descriptor, tag string) error { + return h.printer.Println("Tagged", tag) +} diff --git a/cmd/oras/internal/display/status/utils.go b/cmd/oras/internal/display/status/utils.go index 453a1acc6..860156067 100644 --- a/cmd/oras/internal/display/status/utils.go +++ b/cmd/oras/internal/display/status/utils.go @@ -48,6 +48,14 @@ const ( copyPromptMounted = "Mounted" ) +// Prompts for index events. +const ( + IndexPromptFetching = "Fetching" + IndexPromptFetched = "Fetched " + IndexPromptPacked = "Packed " + IndexPromptPushed = "Pushed " +) + // DeduplicatedFilter filters out deduplicated descriptors. func DeduplicatedFilter(committed *sync.Map) func(desc ocispec.Descriptor) bool { return func(desc ocispec.Descriptor) bool { diff --git a/cmd/oras/root/manifest/cmd.go b/cmd/oras/root/manifest/cmd.go index c68668614..2a84ab048 100644 --- a/cmd/oras/root/manifest/cmd.go +++ b/cmd/oras/root/manifest/cmd.go @@ -17,6 +17,7 @@ package manifest import ( "github.com/spf13/cobra" + "oras.land/oras/cmd/oras/root/manifest/index" ) func Cmd() *cobra.Command { @@ -30,6 +31,7 @@ func Cmd() *cobra.Command { fetchCmd(), fetchConfigCmd(), pushCmd(), + index.Cmd(), ) return cmd } diff --git a/cmd/oras/root/manifest/index/cmd.go b/cmd/oras/root/manifest/index/cmd.go new file mode 100644 index 000000000..54391c123 --- /dev/null +++ b/cmd/oras/root/manifest/index/cmd.go @@ -0,0 +1,30 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "github.com/spf13/cobra" +) + +func Cmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "index [command]", + Short: "[Experimental] Index operations", + } + + cmd.AddCommand( + createCmd(), + ) + return cmd +} diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go new file mode 100644 index 000000000..3e5a2754f --- /dev/null +++ b/cmd/oras/root/manifest/index/create.go @@ -0,0 +1,181 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras/cmd/oras/internal/argument" + "oras.land/oras/cmd/oras/internal/command" + "oras.land/oras/cmd/oras/internal/display" + "oras.land/oras/cmd/oras/internal/display/status" + oerrors "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" + "oras.land/oras/cmd/oras/internal/output" + "oras.land/oras/internal/descriptor" + "oras.land/oras/internal/listener" +) + +var maxConfigSize int64 = 4 * 1024 * 1024 // 4 MiB + +type createOptions struct { + option.Common + option.Target + + sources []string + extraRefs []string +} + +func createCmd() *cobra.Command { + var opts createOptions + cmd := &cobra.Command{ + Use: "create [flags] [:][...]] [{|}...]", + Short: "[Experimental] Create and push an index from provided manifests", + Long: `[Experimental] Create and push an index from provided manifests. All manifests should be in the same repository + +Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push without tagging: + oras manifest index create localhost:5000/hello linux-amd64 linux-arm64 + +Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push with the tag 'v1': + oras manifest index create localhost:5000/hello:v1 linux-amd64 linux-arm64 + +Example - create an index from source manifests using both tags and digests, and push with tag 'v1': + oras manifest index create localhost:5000/hello:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 + +Example - create an index and push it with multiple tags: + oras manifest index create localhost:5000/hello:tag1,tag2,tag3 linux-amd64 linux-arm64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 + +Example - create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1': + oras manifest index create layout-dir:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 +`, + Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination index to create."), + PreRunE: func(cmd *cobra.Command, args []string) error { + refs := strings.Split(args[0], ",") + opts.RawReference = refs[0] + opts.extraRefs = refs[1:] + opts.sources = args[1:] + return option.Parse(cmd, &opts) + }, + Aliases: []string{"pack"}, + RunE: func(cmd *cobra.Command, args []string) error { + return createIndex(cmd, opts) + }, + } + option.ApplyFlags(&opts, cmd.Flags()) + return oerrors.Command(cmd, &opts.Target) +} + +func createIndex(cmd *cobra.Command, opts createOptions) error { + ctx, logger := command.GetLogger(cmd, &opts.Common) + target, err := opts.NewTarget(opts.Common, logger) + if err != nil { + return err + } + manifests, err := fetchSourceManifests(ctx, target, opts) + if err != nil { + return err + } + index := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: manifests, + } + indexBytes, _ := json.Marshal(index) + desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexBytes) + opts.Println(status.IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex) + return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer) +} + +func fetchSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts createOptions) ([]ocispec.Descriptor, error) { + resolved := []ocispec.Descriptor{} + for _, source := range opts.sources { + opts.Println(status.IndexPromptFetching, source) + desc, content, err := oras.FetchBytes(ctx, target, source, oras.DefaultFetchBytesOptions) + if err != nil { + return nil, fmt.Errorf("could not find the manifest %s: %w", source, err) + } + if !descriptor.IsManifest(desc) { + return nil, fmt.Errorf("%s is not a manifest", source) + } + opts.Println(status.IndexPromptFetched, source) + desc = descriptor.Plain(desc) + if descriptor.IsImageManifest(desc) { + desc.Platform, err = getPlatform(ctx, target, content) + if err != nil { + return nil, err + } + } + resolved = append(resolved, desc) + } + return resolved, nil +} + +func getPlatform(ctx context.Context, target oras.ReadOnlyTarget, manifestBytes []byte) (*ocispec.Platform, error) { + // extract config descriptor + var manifest ocispec.Manifest + if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + return nil, err + } + // if config size is larger than 4 MiB, discontinue the fetch + if manifest.Config.Size > maxConfigSize { + return nil, fmt.Errorf("config size %v exceeds MaxBytes %v: %w", manifest.Config.Size, maxConfigSize, errdef.ErrSizeExceedsLimit) + } + // fetch config content + contentBytes, err := content.FetchAll(ctx, target, manifest.Config) + if err != nil { + return nil, err + } + var platform ocispec.Platform + if err := json.Unmarshal(contentBytes, &platform); err != nil || (platform.Architecture == "" && platform.OS == "") { + // ignore if the manifest does not have platform information + return nil, nil + } + return &platform, nil +} + +func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, content []byte, ref string, extraRefs []string, path string, printer *output.Printer) error { + // push the index + var err error + if ref == "" { + err = target.Push(ctx, desc, bytes.NewReader(content)) + } else { + _, err = oras.TagBytes(ctx, target, desc.MediaType, content, ref) + } + if err != nil { + return err + } + printer.Println(status.IndexPromptPushed, path) + if len(extraRefs) != 0 { + handler := display.NewManifestIndexCreateHandler(printer) + tagListener := listener.NewTaggedListener(target, handler.OnTagged) + if _, err = oras.TagBytesN(ctx, tagListener, desc.MediaType, content, extraRefs, oras.DefaultTagBytesNOptions); err != nil { + return err + } + } + return printer.Println("Digest:", desc.Digest) +} diff --git a/internal/descriptor/descriptor.go b/internal/descriptor/descriptor.go index 84806fed2..f884edd99 100644 --- a/internal/descriptor/descriptor.go +++ b/internal/descriptor/descriptor.go @@ -21,6 +21,20 @@ import ( "oras.land/oras/internal/docker" ) +// IsManifest checks if a descriptor describes a manifest. +// Adapted from `oras-go`: https://github.com/oras-project/oras-go/blob/d6c837e439f4c567f8003eab6e423c22900452a8/internal/descriptor/descriptor.go#L67 +func IsManifest(desc ocispec.Descriptor) bool { + switch desc.MediaType { + case docker.MediaTypeManifest, + docker.MediaTypeManifestList, + ocispec.MediaTypeImageManifest, + ocispec.MediaTypeImageIndex: + return true + default: + return false + } +} + // IsImageManifest checks whether a manifest is an image manifest. func IsImageManifest(desc ocispec.Descriptor) bool { return desc.MediaType == docker.MediaTypeManifest || desc.MediaType == ocispec.MediaTypeImageManifest @@ -37,6 +51,16 @@ func ShortDigest(desc ocispec.Descriptor) (digestString string) { return digestString } +// Plain returns a plain descriptor that contains only MediaType, Digest and Size. +// Copied from `oras-go`: https://github.com/oras-project/oras-go/blob/d6c837e439f4c567f8003eab6e423c22900452a8/internal/descriptor/descriptor.go#L81 +func Plain(desc ocispec.Descriptor) ocispec.Descriptor { + return ocispec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} + // GetTitleOrMediaType gets a descriptor name using either title or media type. func GetTitleOrMediaType(desc ocispec.Descriptor) (name string, isTitle bool) { name, ok := desc.Annotations[ocispec.AnnotationTitle] diff --git a/internal/descriptor/descriptor_test.go b/internal/descriptor/descriptor_test.go index 4d01e77a4..392bfa671 100644 --- a/internal/descriptor/descriptor_test.go +++ b/internal/descriptor/descriptor_test.go @@ -56,6 +56,18 @@ func TestDescriptor_IsImageManifest(t *testing.T) { } } +func TestDescriptor_IsManifest(t *testing.T) { + got := descriptor.IsManifest(imageDesc) + if !got { + t.Fatalf("IsManifest() got %v, want %v", got, true) + } + + got = descriptor.IsManifest(artifactDesc) + if got { + t.Fatalf("IsManifest() got %v, want %v", got, false) + } +} + func TestDescriptor_ShortDigest(t *testing.T) { expected := "2e0e0fe1fb3e" got := descriptor.ShortDigest(titledDesc) diff --git a/test/e2e/README.md b/test/e2e/README.md index 2306b256c..ea7431160 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -163,11 +163,18 @@ graph TD; A2-- hello.tar -->A8(blob) A3-- hello.tar -->A8(blob) A4-- hello.tar -->A8(blob) + A9>tag: linux-amd64]-..->A2 + A10>tag: linux-arm64]-..->A3 + A11>tag: linux-armv7]-..->A4 B0>tag: foobar]-..->B1[oci image] B1-- foo1 -->B2(blob1) B1-- foo2 -->B2(blob1) B1-- bar -->B3(blob2) + + C0>tag: nonjson-config]-..->C1[oci image] + C1-->C2(config4) + C1-->C3(blob4) end ``` diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index d4a3ae380..c1062a42c 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -27,6 +27,7 @@ var ( Digest = "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f" Manifest = `{"mediaType":"application/vnd.oci.image.index.v1+json","schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c","size":458,"platform":{"architecture":"arm64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255","size":458,"platform":{"architecture":"arm","os":"linux","variant":"v7"}}]}` Descriptor = `{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f","size":706}` + DescriptorObject = ocispec.Descriptor{MediaType: "application/vnd.oci.image.index.v1+json", Digest: "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f", Size: 706} AnnotatedDescriptor = `{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f","size":706,"annotations":{"org.opencontainers.image.ref.name":"multi"}}` IndexReferrerDigest = "sha256:d37baf66300b9006b0f4c7102075d56b970fbf910be5c6bca07fdbb000dfa383" IndexReferrerStateKey = match.StateKey{Digest: "d3cf790759b0", Name: "application/vnd.oci.image.manifest.v1+json"} @@ -48,6 +49,10 @@ var ( MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.Digest("sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1"), Size: 458, + Platform: &ocispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, } LinuxAMD64DescStr = `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458}` LinuxAMD64IndexDesc = `{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}}` @@ -72,4 +77,23 @@ var ( {Digest: "fe9dbc99451d", Name: "application/vnd.oci.image.config.v1+json"}, {Digest: "2ef548696ac7", Name: "hello.tar"}, } + LinuxARM64 = ocispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest.Digest("sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c"), + Size: 458, + Platform: &ocispec.Platform{ + Architecture: "arm64", + OS: "linux", + }, + } + LinuxARMV7 = ocispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: digest.Digest("sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255"), + Size: 458, + Platform: &ocispec.Platform{ + Architecture: "arm", + OS: "linux", + Variant: "v7", + }, + } ) diff --git a/test/e2e/internal/testdata/nonjson_config/const.go b/test/e2e/internal/testdata/nonjson_config/const.go new file mode 100644 index 000000000..576848327 --- /dev/null +++ b/test/e2e/internal/testdata/nonjson_config/const.go @@ -0,0 +1,28 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nonjson_config + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +var ( + Descriptor = ocispec.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: "sha256:9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2", + Size: 529, + } +) diff --git a/test/e2e/scripts/prepare.sh b/test/e2e/scripts/prepare.sh index a0691eda2..13b119574 100755 --- a/test/e2e/scripts/prepare.sh +++ b/test/e2e/scripts/prepare.sh @@ -62,5 +62,5 @@ docker run --pull always -dp $ZOT_REGISTRY_PORT:5000 \ --name $ZOT_CTR_NAME \ -u $(id -u $(whoami)) \ --mount type=bind,source="${e2e_root}/testdata/zot/",target=/etc/zot \ - --rm ghcr.io/project-zot/zot-linux-amd64:v2.0.1 + --rm ghcr.io/project-zot/zot-linux-amd64:v2.1.1 echo " <<< prepared : zot <<< " diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go new file mode 100644 index 000000000..cfd23e8ac --- /dev/null +++ b/test/e2e/suite/command/manifest_index.go @@ -0,0 +1,228 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package command + +import ( + "encoding/json" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/test/e2e/internal/testdata/multi_arch" + "oras.land/oras/test/e2e/internal/testdata/nonjson_config" + . "oras.land/oras/test/e2e/internal/utils" +) + +var _ = Describe("ORAS beginners:", func() { + When("running manifest index command", func() { + When("running `manifest index create`", func() { + It("should show help doc with alias", func() { + ORAS("manifest", "index", "create", "--help").MatchKeyWords("Aliases", "pack").Exec() + }) + }) + }) +}) + +func indexTestRepo(subcommand string, text string) string { + return fmt.Sprintf("command/index/%d/%s/%s", GinkgoRandomSeed(), subcommand, text) +} + +func ValidateIndex(content []byte, manifests []ocispec.Descriptor) { + var index ocispec.Index + Expect(json.Unmarshal(content, &index)).ShouldNot(HaveOccurred()) + Expect(index.Manifests).To(Equal(manifests)) +} + +var _ = Describe("1.1 registry users:", func() { + When("running `manifest index create`", func() { + It("should create index by using source manifest digests", func() { + testRepo := indexTestRepo("create", "by-digest") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "latest"), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)). + MatchKeyWords("Fetched", "sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1", + "Fetched", "sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c", + "Pushed", "sha256:cce9590b1193d8bcb70467e2381dc81e77869be4801c09abe9bc274b6a1d2001").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "latest")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64, multi_arch.LinuxARM64} + ValidateIndex(content, expectedManifests) + }) + + It("should create index by using source manifest tags", func() { + testRepo := indexTestRepo("create", "by-tag") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "latest"), + "linux-arm64", "linux-amd64"). + MatchKeyWords("Fetched", "linux-arm64", + "Fetched", "linux-amd64", + "Pushed", "sha256:5c98cfc90e390c575679370a5dc5e37b52e854bbb7b9cb80cc1f30b56b8d183e").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "latest")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARM64, multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create index without tagging it", func() { + testRepo := indexTestRepo("create", "no-tag") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), + "linux-arm64", "linux-amd64", "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255"). + MatchKeyWords("Pushed", "sha256:820503ae4fecfdb841b5b6acc8718c8c5b298cf6b8f2259010f370052341cec8").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "sha256:820503ae4fecfdb841b5b6acc8718c8c5b298cf6b8f2259010f370052341cec8")). + Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARM64, multi_arch.LinuxAMD64, multi_arch.LinuxARMV7} + ValidateIndex(content, expectedManifests) + }) + + It("should create index with multiple tags", func() { + testRepo := indexTestRepo("create", "multiple-tags") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", fmt.Sprintf("%s,t1,t2,t3", RegistryRef(ZOTHost, testRepo, "t0")), + "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255", "linux-arm64", "linux-amd64"). + MatchKeyWords("Fetched", "Pushed", "Tagged", + "sha256:bfa1728d6292d5fa7689f8f4daa145ee6f067b5779528c6e059d1132745ef508").Exec() + // verify + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARMV7, multi_arch.LinuxARM64, multi_arch.LinuxAMD64} + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t0")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t1")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t2")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "t3")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + }) + + It("should create nested indexes", func() { + testRepo := indexTestRepo("create", "nested-index") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "nested"), "multi").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "nested")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.DescriptorObject} + ValidateIndex(content, expectedManifests) + }) + + It("should create index from image with non-json config", func() { + testRepo := indexTestRepo("create", "nonjson-config") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "unusual-config"), + "nonjson-config").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "unusual-config")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{nonjson_config.Descriptor} + ValidateIndex(content, expectedManifests) + }) + + It("should fail if given a reference that does not exist in the repo", func() { + testRepo := indexTestRepo("create", "nonexist-ref") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), + "does-not-exist").ExpectFailure(). + MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() + }) + }) +}) + +var _ = Describe("OCI image layout users:", func() { + When("running `manifest index create`", func() { + It("should create an index with source manifest digest", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest)). + WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create an index with source manifest tag", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "linux-amd64"). + WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create an index without tagging it", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "linux-amd64"). + WithWorkDir(root).MatchKeyWords("Digest: sha256:c543059818cb70e6442597a33454ec1e3d3a2bdb526c17875578d33c2ddcf72e").Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "sha256:c543059818cb70e6442597a33454ec1e3d3a2bdb526c17875578d33c2ddcf72e")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + ValidateIndex(content, expectedManifests) + }) + + It("should create an index with multiple tags", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := fmt.Sprintf("%s,t1,t2,t3", LayoutRef(root, "t0")) + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "linux-amd64").WithWorkDir(root).Exec() + // verify + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64} + content := ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t0")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t1")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t2")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + content = ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "t3")).Exec().Out.Contents() + ValidateIndex(content, expectedManifests) + }) + + It("should create nested indexes", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "nested-index") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "multi").WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.DescriptorObject} + ValidateIndex(content, expectedManifests) + }) + + It("should create index from image with non-json config", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "unusual-config") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "nonjson-config").WithWorkDir(root).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{nonjson_config.Descriptor} + ValidateIndex(content, expectedManifests) + }) + + It("should fail if given a reference that does not exist in the repo", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "does-not-exist").ExpectFailure(). + MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() + }) + + It("should fail if given a digest that is not a manifest", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "sha256:02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2").ExpectFailure(). + MatchErrKeyWords("is not a manifest").Exec() + }) + }) +}) diff --git a/test/e2e/testdata/zot/command/images/blobs/sha256/02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2 b/test/e2e/testdata/zot/command/images/blobs/sha256/02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2 new file mode 100644 index 000000000..5236c8555 --- /dev/null +++ b/test/e2e/testdata/zot/command/images/blobs/sha256/02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2 @@ -0,0 +1 @@ +sometext diff --git a/test/e2e/testdata/zot/command/images/blobs/sha256/24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a b/test/e2e/testdata/zot/command/images/blobs/sha256/24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a new file mode 100644 index 000000000..f3147bc70 --- /dev/null +++ b/test/e2e/testdata/zot/command/images/blobs/sha256/24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a @@ -0,0 +1 @@ +my artifact diff --git a/test/e2e/testdata/zot/command/images/blobs/sha256/9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2 b/test/e2e/testdata/zot/command/images/blobs/sha256/9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2 new file mode 100644 index 000000000..44f74ea02 --- /dev/null +++ b/test/e2e/testdata/zot/command/images/blobs/sha256/9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.unknown.config.v1+json","digest":"sha256:02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2","size":9},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:24b9e859bfdff44fbeee998abb782f69fa2b19164464b200351b3be3b690cf4a","size":12,"annotations":{"org.opencontainers.image.title":"artifactfile"}}],"annotations":{"org.opencontainers.image.created":"2024-08-22T08:22:13Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/zot/command/images/index.json b/test/e2e/testdata/zot/command/images/index.json index 2162bc883..9de2694d3 100644 --- a/test/e2e/testdata/zot/command/images/index.json +++ b/test/e2e/testdata/zot/command/images/index.json @@ -1,57 +1,63 @@ { - "schemaVersion": 2, - "manifests": [ - { - "mediaType": "application/vnd.oci.image.index.v1+json", - "digest": "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f", - "size": 706, - "annotations": { - "org.opencontainers.image.ref.name": "multi" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", - "size": 851, - "annotations": { - "org.opencontainers.image.ref.name": "foobar" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255", - "size": 458, - "platform": { - "architecture": "arm", - "os": "linux", - "variant": "v7" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c", - "size": 458, - "platform": { - "architecture": "arm64", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1", - "size": 458, - "platform": { - "architecture": "amd64", - "os": "linux" - } - }, - { - "mediaType": "application/vnd.oci.image.index.v1+json", - "digest": "sha256:b2a5fcfb112ccde647a5a3dc0215c2c9e7d0ce598924a5ec48aa85beca048286", - "size": 89, - "annotations": { - "org.opencontainers.image.ref.name": "empty_index" - } - } - ] + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255", + "size": 458, + "annotations": { + "org.opencontainers.image.ref.name": "linux-armv7" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2", + "size": 529, + "annotations": { + "org.opencontainers.image.created": "2024-08-22T08:22:13Z", + "org.opencontainers.image.ref.name": "nonjson-config" + }, + "artifactType": "application/vnd.unknown.config.v1+json" + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", + "size": 851, + "annotations": { + "org.opencontainers.image.ref.name": "foobar" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:4f93460061882467e6fb3b772dc6ab72130d9ac1906aed2fc7589a5cd145433c", + "size": 458, + "annotations": { + "org.opencontainers.image.ref.name": "linux-arm64" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:b2a5fcfb112ccde647a5a3dc0215c2c9e7d0ce598924a5ec48aa85beca048286", + "size": 89, + "annotations": { + "org.opencontainers.image.ref.name": "empty_index" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:e2bfc9cc6a84ec2d7365b5a28c6bc5806b7fa581c9ad7883be955a64e3cc034f", + "size": 706, + "annotations": { + "org.opencontainers.image.ref.name": "multi" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1", + "size": 458, + "annotations": { + "org.opencontainers.image.ref.name": "linux-amd64" + } + } + ] } \ No newline at end of file From 70ac79b4f44c9b84d64e1191a33f96c32a01f531 Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:26:30 +0800 Subject: [PATCH 10/30] fix: reference in the formatted output of `oras pull` is invalid (#1496) Signed-off-by: Xiaoxuan Wang --- cmd/oras/internal/option/target.go | 22 ++++------------------ test/e2e/suite/command/pull.go | 7 +++++++ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/cmd/oras/internal/option/target.go b/cmd/oras/internal/option/target.go index b81c52b26..489eeb820 100644 --- a/cmd/oras/internal/option/target.go +++ b/cmd/oras/internal/option/target.go @@ -113,6 +113,8 @@ func (opts *Target) Parse(cmd *cobra.Command) error { } } else { opts.Reference = ref.Reference + ref.Reference = "" + opts.Path = ref.String() } return opts.Remote.Parse(cmd) } @@ -145,15 +147,7 @@ func (opts *Target) newOCIStore() (*oci.Store, error) { } func (opts *Target) newRepository(common Common, logger logrus.FieldLogger) (*remote.Repository, error) { - repo, err := opts.NewRepository(opts.RawReference, common, logger) - if err != nil { - return nil, err - } - tmp := repo.Reference - tmp.Reference = "" - opts.Path = tmp.String() - opts.Reference = repo.Reference.Reference - return repo, nil + return opts.NewRepository(opts.RawReference, common, logger) } // NewTarget generates a new target based on opts. @@ -232,15 +226,7 @@ func (opts *Target) NewReadonlyTarget(ctx context.Context, common Common, logger } return store, nil case TargetTypeRemote: - repo, err := opts.NewRepository(opts.RawReference, common, logger) - if err != nil { - return nil, err - } - tmp := repo.Reference - tmp.Reference = "" - opts.Path = tmp.String() - opts.Reference = repo.Reference.Reference - return repo, nil + return opts.NewRepository(opts.RawReference, common, logger) } return nil, fmt.Errorf("unknown target type: %q", opts.Type) } diff --git a/test/e2e/suite/command/pull.go b/test/e2e/suite/command/pull.go index cb9e7a666..d463cf582 100644 --- a/test/e2e/suite/command/pull.go +++ b/test/e2e/suite/command/pull.go @@ -267,6 +267,13 @@ var _ = Describe("OCI spec 1.1 registry users:", func() { Expect(err).ShouldNot(HaveOccurred()) }) + It("should show correct reference", func() { + tempDir := PrepareTempFiles() + ref := RegistryRef(ZOTHost, ArtifactRepo, foobar.Tag) + out := ORAS("pull", ref, "--format", "go-template={{.reference}}").WithWorkDir(tempDir).Exec().Out.Contents() + Expect(out).To(Equal([]byte("localhost:7000/command/artifacts@sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb"))) + }) + It("should pull specific platform", func() { ORAS("pull", RegistryRef(ZOTHost, ImageRepo, "multi"), "--platform", "linux/amd64", "-v", "-o", GinkgoT().TempDir()). MatchStatus(multi_arch.LinuxAMD64StateKeys, true, len(multi_arch.LinuxAMD64StateKeys)).Exec() From d1fe4d0f7f99d0cbcf5d303c2144de99a3fb3894 Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:07:59 +0800 Subject: [PATCH 11/30] docs: add examples for --format (#1497) Signed-off-by: Xiaoxuan Wang --- cmd/oras/root/attach.go | 6 ++++++ cmd/oras/root/discover.go | 16 +++++++++------- cmd/oras/root/manifest/fetch.go | 3 +++ cmd/oras/root/pull.go | 6 ++++++ cmd/oras/root/push.go | 6 ++++++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 89a570210..37d2ddf98 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -77,6 +77,12 @@ Example - Attach an artifact with manifest annotations: Example - Attach file 'hi.txt' and add manifest annotations: oras attach --artifact-type doc/example --annotation "key=val" localhost:5000/hello:v1 hi.txt +Example - Attach file 'hi.txt' and format output in JSON: + oras attach --artifact-type doc/example localhost:5000/hello:v1 hi.txt --format json + +Example - Attach file 'hi.txt' and format output with Go template: + oras attach --artifact-type doc/example localhost:5000/hello:v1 hi.txt --format go-template --template "{{.digest}}" + Example - Attach file 'hi.txt' and export the pushed manifest to 'manifest.json': oras attach --artifact-type doc/example --export-manifest manifest.json localhost:5000/hello:v1 hi.txt diff --git a/cmd/oras/root/discover.go b/cmd/oras/root/discover.go index 8e3f4638d..34d3bccb0 100644 --- a/cmd/oras/root/discover.go +++ b/cmd/oras/root/discover.go @@ -51,27 +51,29 @@ func discoverCmd() *cobra.Command { ** This command is in preview and under development. ** -Example - Discover direct referrers of manifest 'hello:v1' in registry 'localhost:5000': +Example - Discover referrers of manifest 'hello:v1' in registry 'localhost:5000', displayed in a tree view: oras discover localhost:5000/hello:v1 -Example - Discover direct referrers via referrers API: +Example - Discover referrers via referrers API: oras discover --distribution-spec v1.1-referrers-api localhost:5000/hello:v1 -Example - Discover direct referrers via tag scheme: +Example - Discover referrers via tag scheme: oras discover --distribution-spec v1.1-referrers-tag localhost:5000/hello:v1 -Example - Discover all the referrers of manifest 'hello:v1' in registry 'localhost:5000', displayed in a tree view: - oras discover -o tree localhost:5000/hello:v1 +Example - Discover referrers and display in a table view: + oras discover localhost:5000/hello:v1 --format table + +Example - Discover referrers and format output with Go template: + oras discover localhost:5000/hello:v1 --format go-template --template "{{.manifests}}" Example - Discover all the referrers of manifest with annotations, displayed in a tree view: - oras discover -v -o tree localhost:5000/hello:v1 + oras discover -v localhost:5000/hello:v1 Example - Discover referrers with type 'test-artifact' of manifest 'hello:v1' in registry 'localhost:5000': oras discover --artifact-type test-artifact localhost:5000/hello:v1 Example - Discover referrers of the manifest tagged 'v1' in an OCI image layout folder 'layout-dir': oras discover --oci-layout layout-dir:v1 - oras discover --oci-layout -v -o tree layout-dir:v1 `, Args: oerrors.CheckArgs(argument.Exactly(1), "the target artifact to discover referrers from"), PreRunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 8476d12d7..74381cada 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -58,6 +58,9 @@ Example - Fetch the descriptor of a manifest from a registry: Example - Fetch the manifest digest from a registry similar to the resolve command: oras manifest fetch --format go-template --template '{{ .digest }}' localhost:5000/hello:v1 +Example - Fetch manifest and output metadata encoded in JSON: + oras manifest fetch localhost:5000/hello:v1 --format json + Example - Fetch manifest from a registry with specified media type: oras manifest fetch --media-type 'application/vnd.oci.image.manifest.v1+json' localhost:5000/hello:v1 diff --git a/cmd/oras/root/pull.go b/cmd/oras/root/pull.go index 4b01ec779..6ab3a3dff 100644 --- a/cmd/oras/root/pull.go +++ b/cmd/oras/root/pull.go @@ -83,6 +83,12 @@ Example - Pull files from a registry with certain platform: Example - Pull all files with concurrency level tuned: oras pull --concurrency 6 localhost:5000/hello:v1 +Example - Pull files and format output in JSON: + oras pull localhost:5000/hello:v1 --format json + +Example - Pull files and format output with Go template: + oras pull localhost:5000/hello:v1 --format go-template="{{.reference}}" + Example - Pull artifact files from an OCI image layout folder 'layout-dir': oras pull --oci-layout layout-dir:v1 diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 1eb11e523..f8afa6a3c 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -79,6 +79,12 @@ Example - Push file "hi.txt" with config type "application/vnd.me.config": Example - Push file "hi.txt" with the custom manifest config "config.json" of the custom media type "application/vnd.me.config": oras push --config config.json:application/vnd.me.config localhost:5000/hello:v1 hi.txt +Example - Push file "hi.txt" and format output in JSON: + oras push localhost:5000/hello:v1 hi.txt --format json + +Example - Push file "hi.txt" and format output with Go template: + oras push localhost:5000/hello:v1 hi.txt --format go-template="{{.digest}}" + Example - Push file to the insecure registry: oras push --insecure localhost:5000/hello:v1 hi.txt From 0baec3567309373b9304e4040a1ca4f36f1e6f36 Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:04:40 +0800 Subject: [PATCH 12/30] docs: improve the description of cp (#1498) Signed-off-by: Xiaoxuan Wang --- cmd/oras/root/cp.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 9f0fd6fa4..8cf5ac250 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -19,11 +19,12 @@ import ( "context" "encoding/json" "fmt" - "oras.land/oras/cmd/oras/internal/display/status" "slices" "strings" "sync" + "oras.land/oras/cmd/oras/internal/display/status" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" @@ -60,7 +61,7 @@ func copyCmd() *cobra.Command { Use: "cp [flags] {:|@} [:[,][...]]", Aliases: []string{"copy"}, Short: "Copy artifacts from one target to another", - Long: `Copy artifacts from one target to another + Long: `Copy artifacts from one target to another. When copying an image index, all of its manifests will be copied Example - Copy an artifact between registries: oras cp localhost:5000/net-monitor:v1 localhost:6000/net-monitor-copy:v1 From a3b0601052a7bfd3f050dfcbc551b30ba3281154 Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:23:33 +0800 Subject: [PATCH 13/30] feat: --output for `oras manifest index create` (#1490) Signed-off-by: Xiaoxuan Wang --- cmd/oras/root/manifest/index/create.go | 27 ++++++++++++++-- .../e2e/internal/testdata/multi_arch/const.go | 5 +++ test/e2e/suite/command/manifest_index.go | 31 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go index 3e5a2754f..66bdecb6e 100644 --- a/cmd/oras/root/manifest/index/create.go +++ b/cmd/oras/root/manifest/index/create.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "os" "strings" "github.com/opencontainers/image-spec/specs-go" @@ -44,9 +45,11 @@ var maxConfigSize int64 = 4 * 1024 * 1024 // 4 MiB type createOptions struct { option.Common option.Target + option.Pretty - sources []string - extraRefs []string + sources []string + extraRefs []string + outputPath string } func createCmd() *cobra.Command { @@ -70,6 +73,12 @@ Example - create an index and push it with multiple tags: Example - create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1': oras manifest index create layout-dir:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 + +Example - create an index and save it locally to index.json, auto push will be disabled: + oras manifest index create --output index.json localhost:5000/hello linux-amd64 linux-arm64 + +Example - create an index and output the index to stdout, auto push will be disabled: + oras manifest index create localhost:5000/hello linux-arm64 --output - --pretty `, Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination index to create."), PreRunE: func(cmd *cobra.Command, args []string) error { @@ -84,6 +93,7 @@ Example - create an index and push to an OCI image layout folder 'layout-dir' an return createIndex(cmd, opts) }, } + cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the created index to, use - for stdout") option.ApplyFlags(&opts, cmd.Flags()) return oerrors.Command(cmd, &opts.Target) } @@ -108,7 +118,18 @@ func createIndex(cmd *cobra.Command, opts createOptions) error { indexBytes, _ := json.Marshal(index) desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexBytes) opts.Println(status.IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex) - return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer) + + switch opts.outputPath { + case "": + err = pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference(), opts.Printer) + case "-": + opts.Println("Digest:", desc.Digest) + err = opts.Output(os.Stdout, indexBytes) + default: + opts.Println("Digest:", desc.Digest) + err = os.WriteFile(opts.outputPath, indexBytes, 0666) + } + return err } func fetchSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts createOptions) ([]ocispec.Descriptor, error) { diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index c1062a42c..531e6e5c6 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -97,3 +97,8 @@ var ( }, } ) + +// exported index +var ( + CreatedIndex = `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}}]}` +) diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go index cfd23e8ac..fe6d35139 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -18,6 +18,7 @@ package command import ( "encoding/json" "fmt" + "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -130,6 +131,21 @@ var _ = Describe("1.1 registry users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output created index to file", func() { + testRepo := indexTestRepo("create", "output-to-file") + CopyZOTRepo(ImageRepo, testRepo) + filePath := filepath.Join(GinkgoT().TempDir(), "createdIndex") + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.CreatedIndex, DefaultTimeout) + }) + + It("should output created index to stdout", func() { + testRepo := indexTestRepo("create", "output-to-stdout") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), string(multi_arch.LinuxAMD64.Digest), + "--output", "-").MatchKeyWords(multi_arch.CreatedIndex).Exec() + }) + It("should fail if given a reference that does not exist in the repo", func() { testRepo := indexTestRepo("create", "nonexist-ref") CopyZOTRepo(ImageRepo, testRepo) @@ -211,6 +227,21 @@ var _ = Describe("OCI image layout users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output created index to file", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "output-to-file") + filePath := filepath.Join(GinkgoT().TempDir(), "createdIndex") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.CreatedIndex, DefaultTimeout) + }) + + It("should output created index to stdout", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "output-to-stdout") + ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest), + "--output", "-").MatchKeyWords(multi_arch.CreatedIndex).Exec() + }) + It("should fail if given a reference that does not exist in the repo", func() { root := PrepareTempOCI(ImageRepo) indexRef := LayoutRef(root, "latest") From 2808ea189754ba52483501fe0fd9655675e6cc5c Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 12 Sep 2024 11:01:36 -0600 Subject: [PATCH 14/30] feature: build binary for OS and arch by default (#1487) Signed-off-by: Terry Howe Co-authored-by: Billy Zha --- Makefile | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Makefile b/Makefile index ca72c5b4b..355ab30a9 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,19 @@ GIT_COMMIT = $(shell git rev-parse HEAD) GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") GO_EXE = go +OSNAME = $(shell uname -o) +ARCHNAME = $(shell uname -m) + +ifeq ($(OSNAME),Darwin) + OS = mac +else + OS = linux +endif +ifeq ($(ARCHNAME),arm64) + ARCH = arm64 +else + ARCH = amd64 +endif TARGET_OBJS ?= checksums.txt darwin_amd64.tar.gz darwin_arm64.tar.gz linux_amd64.tar.gz linux_arm64.tar.gz linux_armv7.tar.gz linux_s390x.tar.gz linux_ppc64le.tar.gz linux_riscv64.tar.gz windows_amd64.zip freebsd_amd64.tar.gz @@ -31,6 +44,10 @@ endif LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitCommit=${GIT_COMMIT} LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitTreeState=${GIT_DIRTY} +.PHONY: default +default: test build-$(OS)-$(ARCH) + @echo 'Done ' build-$(OS)-$(ARCH) + .PHONY: test test: tidy vendor check-encoding ## tidy and run tests $(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./... From 071f0605bd29303a876f01370b13538a8d77751a Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:50:38 +0800 Subject: [PATCH 15/30] feat: update existing index (index update) (#1476) Signed-off-by: Xiaoxuan Wang --- cmd/oras/internal/display/status/utils.go | 12 +- cmd/oras/root/manifest/index/cmd.go | 1 + cmd/oras/root/manifest/index/create.go | 22 +- cmd/oras/root/manifest/index/update.go | 238 +++++++++++++++++++ cmd/oras/root/manifest/index/update_test.go | 123 ++++++++++ internal/descriptor/descriptor.go | 5 + test/e2e/suite/command/manifest_index.go | 245 ++++++++++++++++++++ 7 files changed, 633 insertions(+), 13 deletions(-) create mode 100644 cmd/oras/root/manifest/index/update.go create mode 100644 cmd/oras/root/manifest/index/update_test.go diff --git a/cmd/oras/internal/display/status/utils.go b/cmd/oras/internal/display/status/utils.go index 860156067..79662a72f 100644 --- a/cmd/oras/internal/display/status/utils.go +++ b/cmd/oras/internal/display/status/utils.go @@ -50,10 +50,14 @@ const ( // Prompts for index events. const ( - IndexPromptFetching = "Fetching" - IndexPromptFetched = "Fetched " - IndexPromptPacked = "Packed " - IndexPromptPushed = "Pushed " + IndexPromptFetching = "Fetching " + IndexPromptFetched = "Fetched " + IndexPromptAdded = "Added " + IndexPromptMerged = "Merged " + IndexPromptRemoved = "Removed " + IndexPromptPacked = "Packed " + IndexPromptPushed = "Pushed " + IndexPromptUpdated = "Updated " ) // DeduplicatedFilter filters out deduplicated descriptors. diff --git a/cmd/oras/root/manifest/index/cmd.go b/cmd/oras/root/manifest/index/cmd.go index 54391c123..3633faa03 100644 --- a/cmd/oras/root/manifest/index/cmd.go +++ b/cmd/oras/root/manifest/index/cmd.go @@ -25,6 +25,7 @@ func Cmd() *cobra.Command { cmd.AddCommand( createCmd(), + updateCmd(), ) return cmd } diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go index 66bdecb6e..1d6164d5d 100644 --- a/cmd/oras/root/manifest/index/create.go +++ b/cmd/oras/root/manifest/index/create.go @@ -36,6 +36,7 @@ import ( oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" + "oras.land/oras/internal/contentutil" "oras.land/oras/internal/descriptor" "oras.land/oras/internal/listener" ) @@ -59,25 +60,25 @@ func createCmd() *cobra.Command { Short: "[Experimental] Create and push an index from provided manifests", Long: `[Experimental] Create and push an index from provided manifests. All manifests should be in the same repository -Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push without tagging: +Example - Create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push without tagging: oras manifest index create localhost:5000/hello linux-amd64 linux-arm64 -Example - create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push with the tag 'v1': +Example - Create an index from source manifests tagged 'linux-amd64' and 'linux-arm64', and push with the tag 'v1': oras manifest index create localhost:5000/hello:v1 linux-amd64 linux-arm64 -Example - create an index from source manifests using both tags and digests, and push with tag 'v1': +Example - Create an index from source manifests using both tags and digests, and push with tag 'v1': oras manifest index create localhost:5000/hello:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 -Example - create an index and push it with multiple tags: +Example - Create an index and push it with multiple tags: oras manifest index create localhost:5000/hello:tag1,tag2,tag3 linux-amd64 linux-arm64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 -Example - create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1': +Example - Create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1': oras manifest index create layout-dir:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 -Example - create an index and save it locally to index.json, auto push will be disabled: +Example - Create an index and save it locally to index.json, auto push will be disabled: oras manifest index create --output index.json localhost:5000/hello linux-amd64 linux-arm64 -Example - create an index and output the index to stdout, auto push will be disabled: +Example - Create an index and output the index to stdout, auto push will be disabled: oras manifest index create localhost:5000/hello linux-arm64 --output - --pretty `, Args: oerrors.CheckArgs(argument.AtLeast(1), "the destination index to create."), @@ -115,7 +116,10 @@ func createIndex(cmd *cobra.Command, opts createOptions) error { MediaType: ocispec.MediaTypeImageIndex, Manifests: manifests, } - indexBytes, _ := json.Marshal(index) + indexBytes, err := json.Marshal(index) + if err != nil { + return err + } desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageIndex, indexBytes) opts.Println(status.IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex) @@ -182,7 +186,7 @@ func getPlatform(ctx context.Context, target oras.ReadOnlyTarget, manifestBytes func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, content []byte, ref string, extraRefs []string, path string, printer *output.Printer) error { // push the index var err error - if ref == "" { + if ref == "" || contentutil.IsDigest(ref) { err = target.Push(ctx, desc, bytes.NewReader(content)) } else { _, err = oras.TagBytes(ctx, target, desc.MediaType, content, ref) diff --git a/cmd/oras/root/manifest/index/update.go b/cmd/oras/root/manifest/index/update.go new file mode 100644 index 000000000..d63b20d49 --- /dev/null +++ b/cmd/oras/root/manifest/index/update.go @@ -0,0 +1,238 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras/cmd/oras/internal/argument" + "oras.land/oras/cmd/oras/internal/command" + "oras.land/oras/cmd/oras/internal/display/status" + oerrors "oras.land/oras/cmd/oras/internal/errors" + "oras.land/oras/cmd/oras/internal/option" + "oras.land/oras/cmd/oras/internal/output" + "oras.land/oras/internal/contentutil" + "oras.land/oras/internal/descriptor" +) + +type updateOptions struct { + option.Common + option.Target + + addArguments []string + mergeArguments []string + removeArguments []string + tags []string +} + +func updateCmd() *cobra.Command { + var opts updateOptions + cmd := &cobra.Command{ + Use: "update {:|@} [{--add|--merge|--remove} {|}] [...]", + Short: "[Experimental] Update and push an image index", + Long: `[Experimental] Update and push an image index. All manifests should be in the same repository + +Example - Remove a manifest and add two manifests from an index tagged 'v1'. The tag will point to the updated index: + oras manifest index update localhost:5000/hello:v1 --add linux-amd64 --add linux-arm64 --remove sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 + +Example - Create a new index by updating an existing index specified by its digest: + oras manifest index update localhost:5000/hello@sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 --add linux-amd64 --remove sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb + +Example - Merge manifests from the index 'v2-windows' to the index 'v2': + oras manifest index update localhost:5000/hello:v2 --merge v2-windows + +Example - Update an index and tag the updated index as 'v2.1.0' and 'v2': + oras manifest index update localhost:5000/hello@sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 --add linux-amd64 --tag "v2.1.0" --tag "v2" + `, + Args: oerrors.CheckArgs(argument.Exactly(1), "the target index to update"), + PreRunE: func(cmd *cobra.Command, args []string) error { + opts.RawReference = args[0] + for _, manifestRef := range opts.removeArguments { + if !contentutil.IsDigest(manifestRef) { + return fmt.Errorf("remove: %s is not a digest", manifestRef) + } + } + return option.Parse(cmd, &opts) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return updateIndex(cmd, opts) + }, + } + option.ApplyFlags(&opts, cmd.Flags()) + cmd.Flags().StringArrayVarP(&opts.addArguments, "add", "", nil, "manifests to add to the index") + cmd.Flags().StringArrayVarP(&opts.mergeArguments, "merge", "", nil, "indexes to be merged into the index") + cmd.Flags().StringArrayVarP(&opts.removeArguments, "remove", "", nil, "manifests to remove from the index, must be digests") + cmd.Flags().StringArrayVarP(&opts.tags, "tag", "", nil, "extra tags for the updated index") + return oerrors.Command(cmd, &opts.Target) +} + +func updateIndex(cmd *cobra.Command, opts updateOptions) error { + // if no update flag is used, do nothing + if !updateFlagsUsed(cmd.Flags()) { + opts.Println("Nothing to update as no change is requested") + return nil + } + ctx, logger := command.GetLogger(cmd, &opts.Common) + target, err := opts.NewTarget(opts.Common, logger) + if err != nil { + return err + } + if err := opts.EnsureReferenceNotEmpty(cmd, true); err != nil { + return err + } + index, err := fetchIndex(ctx, target, opts) + if err != nil { + return err + } + manifests, err := removeManifests(ctx, index.Manifests, target, opts) + if err != nil { + return err + } + manifests, err = addManifests(ctx, manifests, target, opts) + if err != nil { + return err + } + manifests, err = mergeIndexes(ctx, manifests, target, opts) + if err != nil { + return err + } + + index.Manifests = manifests + indexBytes, err := json.Marshal(index) + if err != nil { + return err + } + desc := content.NewDescriptorFromBytes(index.MediaType, indexBytes) + + printUpdateStatus(status.IndexPromptUpdated, string(desc.Digest), "", opts.Printer) + path := getPushPath(opts.RawReference, opts.Type, opts.Reference, opts.Path) + return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.tags, path, opts.Printer) +} + +func fetchIndex(ctx context.Context, target oras.ReadOnlyTarget, opts updateOptions) (ocispec.Index, error) { + printUpdateStatus(status.IndexPromptFetching, opts.Reference, "", opts.Printer) + desc, content, err := oras.FetchBytes(ctx, target, opts.Reference, oras.DefaultFetchBytesOptions) + if err != nil { + return ocispec.Index{}, fmt.Errorf("could not find the index %s: %w", opts.Reference, err) + } + if !descriptor.IsIndex(desc) { + return ocispec.Index{}, fmt.Errorf("%s is not an index", opts.Reference) + } + printUpdateStatus(status.IndexPromptFetched, opts.Reference, string(desc.Digest), opts.Printer) + var index ocispec.Index + if err := json.Unmarshal(content, &index); err != nil { + return ocispec.Index{}, err + } + return index, nil +} + +func addManifests(ctx context.Context, manifests []ocispec.Descriptor, target oras.ReadOnlyTarget, opts updateOptions) ([]ocispec.Descriptor, error) { + for _, manifestRef := range opts.addArguments { + printUpdateStatus(status.IndexPromptFetching, manifestRef, "", opts.Printer) + desc, content, err := oras.FetchBytes(ctx, target, manifestRef, oras.DefaultFetchBytesOptions) + if err != nil { + return nil, fmt.Errorf("could not find the manifest %s: %w", manifestRef, err) + } + if !descriptor.IsManifest(desc) { + return nil, fmt.Errorf("%s is not a manifest", manifestRef) + } + printUpdateStatus(status.IndexPromptFetched, manifestRef, string(desc.Digest), opts.Printer) + if descriptor.IsImageManifest(desc) { + desc.Platform, err = getPlatform(ctx, target, content) + if err != nil { + return nil, err + } + } + manifests = append(manifests, desc) + printUpdateStatus(status.IndexPromptAdded, manifestRef, string(desc.Digest), opts.Printer) + } + return manifests, nil +} + +func mergeIndexes(ctx context.Context, manifests []ocispec.Descriptor, target oras.ReadOnlyTarget, opts updateOptions) ([]ocispec.Descriptor, error) { + for _, indexRef := range opts.mergeArguments { + printUpdateStatus(status.IndexPromptFetching, indexRef, "", opts.Printer) + desc, content, err := oras.FetchBytes(ctx, target, indexRef, oras.DefaultFetchBytesOptions) + if err != nil { + return nil, fmt.Errorf("could not find the index %s: %w", indexRef, err) + } + if !descriptor.IsIndex(desc) { + return nil, fmt.Errorf("%s is not an index", indexRef) + } + printUpdateStatus(status.IndexPromptFetched, indexRef, string(desc.Digest), opts.Printer) + var index ocispec.Index + if err := json.Unmarshal(content, &index); err != nil { + return nil, err + } + manifests = append(manifests, index.Manifests...) + printUpdateStatus(status.IndexPromptMerged, indexRef, string(desc.Digest), opts.Printer) + } + return manifests, nil +} + +func removeManifests(ctx context.Context, manifests []ocispec.Descriptor, target oras.ReadOnlyTarget, opts updateOptions) ([]ocispec.Descriptor, error) { + // create a set of digests to speed up the remove + digestToRemove := make(map[digest.Digest]bool) + for _, manifestRef := range opts.removeArguments { + digestToRemove[digest.Digest(manifestRef)] = false + } + return doRemoveManifests(manifests, digestToRemove, opts.Printer, opts.Reference) +} + +func doRemoveManifests(originalManifests []ocispec.Descriptor, digestToRemove map[digest.Digest]bool, printer *output.Printer, indexRef string) ([]ocispec.Descriptor, error) { + manifests := []ocispec.Descriptor{} + for _, m := range originalManifests { + if _, exists := digestToRemove[m.Digest]; exists { + digestToRemove[m.Digest] = true + } else { + manifests = append(manifests, m) + } + } + for digest, removed := range digestToRemove { + if !removed { + return nil, fmt.Errorf("%s does not exist in the index %s", digest, indexRef) + } + printUpdateStatus(status.IndexPromptRemoved, string(digest), "", printer) + } + return manifests, nil +} + +func updateFlagsUsed(flags *pflag.FlagSet) bool { + return flags.Changed("add") || flags.Changed("remove") || flags.Changed("merge") +} + +func printUpdateStatus(verb string, reference string, resolvedDigest string, printer *output.Printer) { + if resolvedDigest == "" || contentutil.IsDigest(reference) { + printer.Println(verb, reference) + } else { + printer.Println(verb, resolvedDigest, reference) + } +} + +func getPushPath(rawReference string, targetType string, reference string, path string) string { + if contentutil.IsDigest(reference) { + return fmt.Sprintf("[%s] %s", targetType, path) + } + return fmt.Sprintf("[%s] %s", targetType, rawReference) +} diff --git a/cmd/oras/root/manifest/index/update_test.go b/cmd/oras/root/manifest/index/update_test.go new file mode 100644 index 000000000..ed919727e --- /dev/null +++ b/cmd/oras/root/manifest/index/update_test.go @@ -0,0 +1,123 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package index + +import ( + "os" + "reflect" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras/cmd/oras/internal/output" +) + +var ( + A = ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Size: 16, + Digest: "sha256:58efe73e78fe043ca31b89007a025c594ce12aa7e6da27d21c7b14b50112e255", + } + B = ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Size: 18, + Digest: "sha256:9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2", + } + C = ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Size: 19, + Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", + } +) + +func Test_doRemoveManifests(t *testing.T) { + tests := []struct { + name string + manifests []ocispec.Descriptor + digestSet map[digest.Digest]bool + printer *output.Printer + indexRef string + want []ocispec.Descriptor + wantErr bool + }{ + { + name: "remove one matched item", + manifests: []ocispec.Descriptor{A, B, C}, + digestSet: map[digest.Digest]bool{B.Digest: false}, + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + indexRef: "test01", + want: []ocispec.Descriptor{A, C}, + wantErr: false, + }, + { + name: "remove all matched items", + manifests: []ocispec.Descriptor{A, B, A, C, A, A, A}, + digestSet: map[digest.Digest]bool{A.Digest: false}, + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + indexRef: "test02", + want: []ocispec.Descriptor{B, C}, + wantErr: false, + }, + { + name: "remove correctly when there is only one item", + manifests: []ocispec.Descriptor{A}, + digestSet: map[digest.Digest]bool{A.Digest: false}, + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + indexRef: "test03", + want: []ocispec.Descriptor{}, + wantErr: false, + }, + { + name: "remove multiple distinct manifests", + manifests: []ocispec.Descriptor{A, B, C}, + digestSet: map[digest.Digest]bool{A.Digest: false, C.Digest: false}, + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + indexRef: "test04", + want: []ocispec.Descriptor{B}, + wantErr: false, + }, + { + name: "remove multiple duplicate manifests", + manifests: []ocispec.Descriptor{A, B, C, C, B, A, B}, + digestSet: map[digest.Digest]bool{A.Digest: false, C.Digest: false}, + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + indexRef: "test04", + want: []ocispec.Descriptor{B, B, B}, + wantErr: false, + }, + { + name: "return error when deleting a nonexistent item", + manifests: []ocispec.Descriptor{A, C}, + digestSet: map[digest.Digest]bool{B.Digest: false}, + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + indexRef: "test04", + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := doRemoveManifests(tt.manifests, tt.digestSet, tt.printer, tt.indexRef) + if (err != nil) != tt.wantErr { + t.Errorf("removeManifestsFromIndex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("removeManifestsFromIndex() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/descriptor/descriptor.go b/internal/descriptor/descriptor.go index f884edd99..41f7321b7 100644 --- a/internal/descriptor/descriptor.go +++ b/internal/descriptor/descriptor.go @@ -40,6 +40,11 @@ func IsImageManifest(desc ocispec.Descriptor) bool { return desc.MediaType == docker.MediaTypeManifest || desc.MediaType == ocispec.MediaTypeImageManifest } +// IsIndex checks if a descriptor describes an image index or Docker manifest list. +func IsIndex(desc ocispec.Descriptor) bool { + return desc.MediaType == ocispec.MediaTypeImageIndex || desc.MediaType == docker.MediaTypeManifestList +} + // ShortDigest converts the digest of the descriptor to a short form for displaying. func ShortDigest(desc ocispec.Descriptor) (digestString string) { digestString = desc.Digest.String() diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go index fe6d35139..95eee3165 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -36,6 +36,12 @@ var _ = Describe("ORAS beginners:", func() { }) }) }) + + When("running `manifest index update`", func() { + It("should show help doc with --tag flag", func() { + ORAS("manifest", "index", "update", "--help").MatchKeyWords("--tag", "tags for the updated index").Exec() + }) + }) }) func indexTestRepo(subcommand string, text string) string { @@ -154,6 +160,147 @@ var _ = Describe("1.1 registry users:", func() { MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() }) }) + + When("running `manifest index update`", func() { + It("should update by specifying the index tag", func() { + testRepo := indexTestRepo("update", "by-index-tag") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "latest"), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "latest"), + "--add", string(multi_arch.LinuxARMV7.Digest)). + MatchKeyWords("sha256:84887718c9e61daa0f1996aad3ae2eb10db15dcbdab394e4b2dfee7967c55f2c").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "latest")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64, multi_arch.LinuxARM64, multi_arch.LinuxARMV7} + ValidateIndex(content, expectedManifests) + }) + + It("should update by specifying the index digest", func() { + testRepo := indexTestRepo("update", "by-index-digest") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "sha256:cce9590b1193d8bcb70467e2381dc81e77869be4801c09abe9bc274b6a1d2001"), + "--add", string(multi_arch.LinuxARMV7.Digest)). + MatchKeyWords("sha256:84887718c9e61daa0f1996aad3ae2eb10db15dcbdab394e4b2dfee7967c55f2c").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "sha256:84887718c9e61daa0f1996aad3ae2eb10db15dcbdab394e4b2dfee7967c55f2c")). + Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64, multi_arch.LinuxARM64, multi_arch.LinuxARMV7} + ValidateIndex(content, expectedManifests) + }) + + It("should update by add, merge and remove flags", func() { + testRepo := indexTestRepo("update", "all-flags") + CopyZOTRepo(ImageRepo, testRepo) + // create indexes for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "index01"), + string(multi_arch.LinuxAMD64.Digest)).Exec() + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "index02"), + string(multi_arch.LinuxARM64.Digest)).Exec() + // update index with add, merge and remove flags + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "index01"), + "--add", string(multi_arch.LinuxARMV7.Digest), "--merge", "index02", + "--remove", string(multi_arch.LinuxAMD64.Digest)).Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "index01")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARMV7, multi_arch.LinuxARM64} + ValidateIndex(content, expectedManifests) + }) + + It("should update and tag the updated index by --tag flag", func() { + testRepo := indexTestRepo("update", "tag-updated-index") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "sha256:cce9590b1193d8bcb70467e2381dc81e77869be4801c09abe9bc274b6a1d2001"), + "--add", string(multi_arch.LinuxARMV7.Digest), "--tag", "updated"). + MatchKeyWords("sha256:84887718c9e61daa0f1996aad3ae2eb10db15dcbdab394e4b2dfee7967c55f2c").Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "updated")). + Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64, multi_arch.LinuxARM64, multi_arch.LinuxARMV7} + ValidateIndex(content, expectedManifests) + }) + + It("should tell user nothing to update if no update flags are used", func() { + testRepo := indexTestRepo("update", "no-flags") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "nothing-to-update")). + MatchKeyWords("nothing to update").Exec() + }) + + It("should fail if empty reference is given", func() { + testRepo := indexTestRepo("update", "empty-reference") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, ""), + "--add", string(multi_arch.LinuxARMV7.Digest)).ExpectFailure(). + MatchErrKeyWords("Error:", "no tag or digest specified").Exec() + }) + + It("should fail if a wrong reference is given as the index to update", func() { + testRepo := indexTestRepo("update", "wrong-index-ref") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "does-not-exist"), + "--add", string(multi_arch.LinuxARMV7.Digest)).ExpectFailure(). + MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() + }) + + It("should fail if a wrong reference is given as the manifest to add", func() { + testRepo := indexTestRepo("update", "wrong-add-ref") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "add-wrong-tag"), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "add-wrong-tag"), + "--add", "does-not-exist").ExpectFailure(). + MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() + }) + + It("should fail if a wrong reference is given as the index to merge", func() { + testRepo := indexTestRepo("update", "wrong-merge-ref") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "merge-wrong-tag"), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "merge-wrong-tag"), + "--merge", "does-not-exist").ExpectFailure(). + MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() + }) + + It("should fail if a non-digest reference is given as the manifest to remove", func() { + testRepo := indexTestRepo("update", "remove-by-tag") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "remove-by-tag"), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "remove-by-tag"), + "--remove", "latest").ExpectFailure(). + MatchErrKeyWords("Error", "latest", "is not a digest").Exec() + }) + + It("should fail if delete a manifest that does not exist in the index", func() { + testRepo := indexTestRepo("update", "wrong-remove-ref-index") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "remove-not-exist"), + string(multi_arch.LinuxAMD64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "remove-not-exist"), + "--remove", string(multi_arch.LinuxARM64.Digest)).ExpectFailure(). + MatchErrKeyWords("Error", "does not exist").Exec() + }) + }) }) var _ = Describe("OCI image layout users:", func() { @@ -256,4 +403,102 @@ var _ = Describe("OCI image layout users:", func() { MatchErrKeyWords("is not a manifest").Exec() }) }) + + When("running `manifest index update`", func() { + It("should update by specifying the index tag", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, indexRef, + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, indexRef, + "--add", string(multi_arch.LinuxARMV7.Digest)). + MatchKeyWords("sha256:84887718c9e61daa0f1996aad3ae2eb10db15dcbdab394e4b2dfee7967c55f2c").Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64, multi_arch.LinuxARM64, multi_arch.LinuxARMV7} + ValidateIndex(content, expectedManifests) + }) + + It("should update by specifying the index digest", func() { + root := PrepareTempOCI(ImageRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, LayoutRef(root, ""), + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, LayoutRef(root, "sha256:cce9590b1193d8bcb70467e2381dc81e77869be4801c09abe9bc274b6a1d2001"), + "--add", string(multi_arch.LinuxARMV7.Digest)). + MatchKeyWords("sha256:84887718c9e61daa0f1996aad3ae2eb10db15dcbdab394e4b2dfee7967c55f2c").Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "sha256:84887718c9e61daa0f1996aad3ae2eb10db15dcbdab394e4b2dfee7967c55f2c")). + Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxAMD64, multi_arch.LinuxARM64, multi_arch.LinuxARMV7} + ValidateIndex(content, expectedManifests) + }) + + It("should update by add, merge and remove flags", func() { + root := PrepareTempOCI(ImageRepo) + // create indexes for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, LayoutRef(root, "index01"), + string(multi_arch.LinuxAMD64.Digest)).Exec() + ORAS("manifest", "index", "create", Flags.Layout, LayoutRef(root, "index02"), + string(multi_arch.LinuxARM64.Digest)).Exec() + // update index with add, merge and remove flags + ORAS("manifest", "index", "update", Flags.Layout, LayoutRef(root, "index01"), + "--add", string(multi_arch.LinuxARMV7.Digest), "--merge", "index02", + "--remove", string(multi_arch.LinuxAMD64.Digest)).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, LayoutRef(root, "index01")).Exec().Out.Contents() + expectedManifests := []ocispec.Descriptor{multi_arch.LinuxARMV7, multi_arch.LinuxARM64} + ValidateIndex(content, expectedManifests) + }) + + It("should tell user nothing to update if no update flags are used", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + ORAS("manifest", "index", "update", Flags.Layout, indexRef). + MatchKeyWords("nothing to update").Exec() + }) + + It("should fail if empty reference is given", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "") + ORAS("manifest", "index", "update", Flags.Layout, indexRef, + "--add", string(multi_arch.LinuxARMV7.Digest)).ExpectFailure(). + MatchErrKeyWords("Error:", "no tag or digest specified").Exec() + }) + + It("should fail if a non-index reference is given as the index to update", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "linux-amd64") + ORAS("manifest", "index", "update", Flags.Layout, indexRef, + "--add", string(multi_arch.LinuxARMV7.Digest)).ExpectFailure(). + MatchErrKeyWords("Error", "is not an index").Exec() + }) + + It("should fail if a non-manifest reference is given as the manifest to add", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, indexRef, + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, indexRef, + "--add", "sha256:02c15a8d1735c65bb8ca86c716615d3c0d8beb87dc68ed88bb49192f90b184e2").ExpectFailure(). + MatchErrKeyWords("Error", "is not a manifest").Exec() + }) + + It("should fail if a wrong reference is given as the index to merge", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "latest") + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, indexRef, + string(multi_arch.LinuxAMD64.Digest), string(multi_arch.LinuxARM64.Digest)).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, indexRef, + "--merge", "linux-amd64").ExpectFailure(). + MatchErrKeyWords("Error", "is not an index").Exec() + }) + }) }) From 961e9f85b2c5a771f7674513cd1393f2e18e85ea Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:38:42 +0800 Subject: [PATCH 16/30] feat: `--output` for oras manifest `index update` (#1502) Signed-off-by: Xiaoxuan Wang --- cmd/oras/root/manifest/index/update.go | 25 +++++++- .../e2e/internal/testdata/multi_arch/const.go | 2 +- test/e2e/suite/command/manifest_index.go | 59 +++++++++++++++++-- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/cmd/oras/root/manifest/index/update.go b/cmd/oras/root/manifest/index/update.go index d63b20d49..d8a43b07c 100644 --- a/cmd/oras/root/manifest/index/update.go +++ b/cmd/oras/root/manifest/index/update.go @@ -19,6 +19,7 @@ import ( "context" "encoding/json" "fmt" + "os" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -39,11 +40,13 @@ import ( type updateOptions struct { option.Common option.Target + option.Pretty addArguments []string mergeArguments []string removeArguments []string tags []string + outputPath string } func updateCmd() *cobra.Command { @@ -64,9 +67,18 @@ Example - Merge manifests from the index 'v2-windows' to the index 'v2': Example - Update an index and tag the updated index as 'v2.1.0' and 'v2': oras manifest index update localhost:5000/hello@sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 --add linux-amd64 --tag "v2.1.0" --tag "v2" + +Example - Update an index and save it locally to index.json, auto push will be disabled: + oras manifest index update --output index.json localhost:5000/hello:v2 --add v2-linux-amd64 + +Example - Update an index and output the index to stdout, auto push will be disabled: + oras manifest index update --output - --pretty localhost:5000/hello:v2 --remove sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 `, Args: oerrors.CheckArgs(argument.Exactly(1), "the target index to update"), PreRunE: func(cmd *cobra.Command, args []string) error { + if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), "tag", "output"); err != nil { + return err + } opts.RawReference = args[0] for _, manifestRef := range opts.removeArguments { if !contentutil.IsDigest(manifestRef) { @@ -84,6 +96,7 @@ Example - Update an index and tag the updated index as 'v2.1.0' and 'v2': cmd.Flags().StringArrayVarP(&opts.mergeArguments, "merge", "", nil, "indexes to be merged into the index") cmd.Flags().StringArrayVarP(&opts.removeArguments, "remove", "", nil, "manifests to remove from the index, must be digests") cmd.Flags().StringArrayVarP(&opts.tags, "tag", "", nil, "extra tags for the updated index") + cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the created index to, use - for stdout") return oerrors.Command(cmd, &opts.Target) } @@ -127,7 +140,17 @@ func updateIndex(cmd *cobra.Command, opts updateOptions) error { printUpdateStatus(status.IndexPromptUpdated, string(desc.Digest), "", opts.Printer) path := getPushPath(opts.RawReference, opts.Type, opts.Reference, opts.Path) - return pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.tags, path, opts.Printer) + switch opts.outputPath { + case "": + err = pushIndex(ctx, target, desc, indexBytes, opts.Reference, opts.tags, path, opts.Printer) + case "-": + opts.Println("Digest:", desc.Digest) + err = opts.Output(os.Stdout, indexBytes) + default: + opts.Println("Digest:", desc.Digest) + err = os.WriteFile(opts.outputPath, indexBytes, 0666) + } + return err } func fetchIndex(ctx context.Context, target oras.ReadOnlyTarget, opts updateOptions) (ocispec.Index, error) { diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index 531e6e5c6..f8a1f3d8b 100644 --- a/test/e2e/internal/testdata/multi_arch/const.go +++ b/test/e2e/internal/testdata/multi_arch/const.go @@ -100,5 +100,5 @@ var ( // exported index var ( - CreatedIndex = `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}}]}` + OutputIndex = `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9d84a5716c66a1d1b9c13f8ed157ba7d1edfe7f9b8766728b8a1f25c0d9c14c1","size":458,"platform":{"architecture":"amd64","os":"linux"}}]}` ) diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go index 95eee3165..d6da1797a 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -142,14 +142,14 @@ var _ = Describe("1.1 registry users:", func() { CopyZOTRepo(ImageRepo, testRepo) filePath := filepath.Join(GinkgoT().TempDir(), "createdIndex") ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() - MatchFile(filePath, multi_arch.CreatedIndex, DefaultTimeout) + MatchFile(filePath, multi_arch.OutputIndex, DefaultTimeout) }) It("should output created index to stdout", func() { testRepo := indexTestRepo("create", "output-to-stdout") CopyZOTRepo(ImageRepo, testRepo) ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), string(multi_arch.LinuxAMD64.Digest), - "--output", "-").MatchKeyWords(multi_arch.CreatedIndex).Exec() + "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() }) It("should fail if given a reference that does not exist in the repo", func() { @@ -230,6 +230,28 @@ var _ = Describe("1.1 registry users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output updated index to file", func() { + testRepo := indexTestRepo("update", "output-to-file") + CopyZOTRepo(ImageRepo, testRepo) + filePath := filepath.Join(GinkgoT().TempDir(), "updatedIndex") + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "v1")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "v1"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.OutputIndex, DefaultTimeout) + }) + + It("should output updated index to stdout", func() { + testRepo := indexTestRepo("update", "output-to-stdout") + CopyZOTRepo(ImageRepo, testRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "v1")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "v1"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() + }) + It("should tell user nothing to update if no update flags are used", func() { testRepo := indexTestRepo("update", "no-flags") CopyZOTRepo(ImageRepo, testRepo) @@ -300,6 +322,15 @@ var _ = Describe("1.1 registry users:", func() { "--remove", string(multi_arch.LinuxARM64.Digest)).ExpectFailure(). MatchErrKeyWords("Error", "does not exist").Exec() }) + + It("should fail if --tag is used with --output", func() { + testRepo := indexTestRepo("update", "tag-and-output") + CopyZOTRepo(ImageRepo, testRepo) + // add a manifest to the index + ORAS("manifest", "index", "update", RegistryRef(ZOTHost, testRepo, "v1"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-", "--tag", "v2"). + ExpectFailure().MatchErrKeyWords("--tag, --output cannot be used at the same time").Exec() + }) }) }) @@ -379,14 +410,14 @@ var _ = Describe("OCI image layout users:", func() { indexRef := LayoutRef(root, "output-to-file") filePath := filepath.Join(GinkgoT().TempDir(), "createdIndex") ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() - MatchFile(filePath, multi_arch.CreatedIndex, DefaultTimeout) + MatchFile(filePath, multi_arch.OutputIndex, DefaultTimeout) }) It("should output created index to stdout", func() { root := PrepareTempOCI(ImageRepo) indexRef := LayoutRef(root, "output-to-stdout") ORAS("manifest", "index", "create", Flags.Layout, indexRef, string(multi_arch.LinuxAMD64.Digest), - "--output", "-").MatchKeyWords(multi_arch.CreatedIndex).Exec() + "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() }) It("should fail if given a reference that does not exist in the repo", func() { @@ -454,6 +485,26 @@ var _ = Describe("OCI image layout users:", func() { ValidateIndex(content, expectedManifests) }) + It("should output updated index to file", func() { + root := PrepareTempOCI(ImageRepo) + filePath := filepath.Join(GinkgoT().TempDir(), "updatedIndex") + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, LayoutRef(root, "index01")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, LayoutRef(root, "index01"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", filePath).Exec() + MatchFile(filePath, multi_arch.OutputIndex, DefaultTimeout) + }) + + It("should output updated index to stdout", func() { + root := PrepareTempOCI(ImageRepo) + // create an index for testing purpose + ORAS("manifest", "index", "create", Flags.Layout, LayoutRef(root, "index01")).Exec() + // add a manifest to the index + ORAS("manifest", "index", "update", Flags.Layout, LayoutRef(root, "index01"), + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-").MatchKeyWords(multi_arch.OutputIndex).Exec() + }) + It("should tell user nothing to update if no update flags are used", func() { root := PrepareTempOCI(ImageRepo) indexRef := LayoutRef(root, "latest") From 8dc05a721c5e1bf1d919ed470b1a0e6f4e404b85 Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:53:59 +0800 Subject: [PATCH 17/30] feat: `--annotation` for `index create` (#1499) Signed-off-by: Xiaoxuan Wang --- cmd/oras/internal/option/annotation.go | 67 ++++++++++++++++++++++++ cmd/oras/internal/option/packer.go | 54 ++++++------------- cmd/oras/internal/option/packer_test.go | 67 +++++++++++++++--------- cmd/oras/root/attach.go | 10 ++-- cmd/oras/root/attach_test.go | 10 ++-- cmd/oras/root/manifest/index/create.go | 9 +++- cmd/oras/root/push.go | 10 ++-- test/e2e/suite/command/manifest_index.go | 34 ++++++++++++ 8 files changed, 179 insertions(+), 82 deletions(-) create mode 100644 cmd/oras/internal/option/annotation.go diff --git a/cmd/oras/internal/option/annotation.go b/cmd/oras/internal/option/annotation.go new file mode 100644 index 000000000..035307a40 --- /dev/null +++ b/cmd/oras/internal/option/annotation.go @@ -0,0 +1,67 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + oerrors "oras.land/oras/cmd/oras/internal/errors" +) + +var ( + errAnnotationFormat = errors.New("annotation value doesn't match the required format") + errAnnotationDuplication = errors.New("duplicate annotation key") +) + +// Annotation option struct. +type Annotation struct { + // ManifestAnnotations contains raw input of manifest annotation "key=value" pairs + ManifestAnnotations []string + + // Annotations contains parsed manifest and config annotations + Annotations map[string]map[string]string +} + +// ApplyFlags applies flags to a command flag set. +func (opts *Annotation) ApplyFlags(fs *pflag.FlagSet) { + fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") +} + +// Parse parses the input annotation flags. +func (opts *Annotation) Parse(*cobra.Command) error { + manifestAnnotations := make(map[string]string) + for _, anno := range opts.ManifestAnnotations { + key, val, success := strings.Cut(anno, "=") + if !success { + return &oerrors.Error{ + Err: errAnnotationFormat, + Recommendation: `Please use the correct format in the flag: --annotation "key=value"`, + } + } + if _, ok := manifestAnnotations[key]; ok { + return fmt.Errorf("%w: %v, ", errAnnotationDuplication, key) + } + manifestAnnotations[key] = val + } + opts.Annotations = map[string]map[string]string{ + AnnotationManifest: manifestAnnotations, + } + return nil +} diff --git a/cmd/oras/internal/option/packer.go b/cmd/oras/internal/option/packer.go index 7217df5ee..a69b12f3a 100644 --- a/cmd/oras/internal/option/packer.go +++ b/cmd/oras/internal/option/packer.go @@ -39,26 +39,26 @@ const ( ) var ( - errAnnotationConflict = errors.New("`--annotation` and `--annotation-file` cannot be both specified") - errAnnotationFormat = errors.New("annotation value doesn't match the required format") - errAnnotationDuplication = errors.New("duplicate annotation key") - errPathValidation = errors.New("absolute file path detected. If it's intentional, use --disable-path-validation flag to skip this check") + errAnnotationConflict = errors.New("`--annotation` and `--annotation-file` cannot be both specified") + errPathValidation = errors.New("absolute file path detected. If it's intentional, use --disable-path-validation flag to skip this check") ) // Packer option struct. type Packer struct { + Annotation + ManifestExportPath string PathValidationDisabled bool AnnotationFilePath string - ManifestAnnotations []string FileRefs []string } // ApplyFlags applies flags to a command flag set. func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { + opts.Annotation.ApplyFlags(fs) + fs.StringVarP(&opts.ManifestExportPath, "export-manifest", "", "", "`path` of the pushed manifest") - fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") fs.StringVarP(&opts.AnnotationFilePath, "annotation-file", "", "", "path of the annotation file") fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") } @@ -74,7 +74,8 @@ func (opts *Packer) ExportManifest(ctx context.Context, fetcher content.Fetcher, } return os.WriteFile(opts.ManifestExportPath, manifestBytes, 0666) } -func (opts *Packer) Parse(*cobra.Command) error { + +func (opts *Packer) Parse(cmd *cobra.Command) error { if !opts.PathValidationDisabled { var failedPaths []string for _, path := range opts.FileRefs { @@ -91,29 +92,26 @@ func (opts *Packer) Parse(*cobra.Command) error { return fmt.Errorf("%w: %v", errPathValidation, strings.Join(failedPaths, ", ")) } } - return nil + return opts.parseAnnotations(cmd) } -// LoadManifestAnnotations loads the manifest annotation map. -func (opts *Packer) LoadManifestAnnotations() (annotations map[string]map[string]string, err error) { +// parseAnnotations loads the manifest annotation map. +func (opts *Packer) parseAnnotations(cmd *cobra.Command) error { if opts.AnnotationFilePath != "" && len(opts.ManifestAnnotations) != 0 { - return nil, errAnnotationConflict + return errAnnotationConflict } if opts.AnnotationFilePath != "" { - if err = decodeJSON(opts.AnnotationFilePath, &annotations); err != nil { - return nil, &oerrors.Error{ + if err := decodeJSON(opts.AnnotationFilePath, &opts.Annotations); err != nil { + return &oerrors.Error{ Err: fmt.Errorf(`invalid annotation json file: failed to load annotations from %s`, opts.AnnotationFilePath), Recommendation: `Annotation file doesn't match the required format. Please refer to the document at https://oras.land/docs/how_to_guides/manifest_annotations`, } } } if len(opts.ManifestAnnotations) != 0 { - annotations = make(map[string]map[string]string) - if err = parseAnnotationFlags(opts.ManifestAnnotations, annotations); err != nil { - return nil, err - } + return opts.Annotation.Parse(cmd) } - return + return nil } // decodeJSON decodes a json file v to filename. @@ -125,23 +123,3 @@ func decodeJSON(filename string, v interface{}) error { defer file.Close() return json.NewDecoder(file).Decode(v) } - -// parseAnnotationFlags parses annotation flags into a map. -func parseAnnotationFlags(flags []string, annotations map[string]map[string]string) error { - manifestAnnotations := make(map[string]string) - for _, anno := range flags { - key, val, success := strings.Cut(anno, "=") - if !success { - return &oerrors.Error{ - Err: errAnnotationFormat, - Recommendation: `Please use the correct format in the flag: --annotation "key=value"`, - } - } - if _, ok := manifestAnnotations[key]; ok { - return fmt.Errorf("%w: %v, ", errAnnotationDuplication, key) - } - manifestAnnotations[key] = val - } - annotations[AnnotationManifest] = manifestAnnotations - return nil -} diff --git a/cmd/oras/internal/option/packer_test.go b/cmd/oras/internal/option/packer_test.go index e0cd51339..35248ccd7 100644 --- a/cmd/oras/internal/option/packer_test.go +++ b/cmd/oras/internal/option/packer_test.go @@ -37,62 +37,73 @@ func TestPacker_FlagInit(t *testing.T) { ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) } -func TestPacker_LoadManifestAnnotations_err(t *testing.T) { +func TestPacker_parseAnnotations_err(t *testing.T) { opts := Packer{ - AnnotationFilePath: "this is not a file", // testFile, - ManifestAnnotations: []string{"Key=Val"}, + Annotation: Annotation{ + ManifestAnnotations: []string{"Key=Val"}, + }, + AnnotationFilePath: "this is not a file", // testFile, } - if _, err := opts.LoadManifestAnnotations(); !errors.Is(err, errAnnotationConflict) { + if err := opts.parseAnnotations(nil); !errors.Is(err, errAnnotationConflict) { t.Fatalf("unexpected error: %v", err) } opts = Packer{ AnnotationFilePath: "this is not a file", // testFile, } - if _, err := opts.LoadManifestAnnotations(); err == nil { + if err := opts.parseAnnotations(nil); err == nil { t.Fatalf("unexpected error: %v", err) } opts = Packer{ - ManifestAnnotations: []string{"KeyVal"}, + Annotation: Annotation{ + ManifestAnnotations: []string{"KeyVal"}, + }, } - if _, err := opts.LoadManifestAnnotations(); !errors.Is(err, errAnnotationFormat) { + if err := opts.parseAnnotations(nil); !errors.Is(err, errAnnotationFormat) { t.Fatalf("unexpected error: %v", err) } opts = Packer{ - ManifestAnnotations: []string{"Key=Val1", "Key=Val2"}, + Annotation: Annotation{ + ManifestAnnotations: []string{"Key=Val1", "Key=Val2"}, + }, } - if _, err := opts.LoadManifestAnnotations(); !errors.Is(err, errAnnotationDuplication) { + if err := opts.parseAnnotations(nil); !errors.Is(err, errAnnotationDuplication) { t.Fatalf("unexpected error: %v", err) } } -func TestPacker_LoadManifestAnnotations_annotationFile(t *testing.T) { +func TestPacker_parseAnnotations_annotationFile(t *testing.T) { testFile := filepath.Join(t.TempDir(), "testAnnotationFile") err := os.WriteFile(testFile, []byte(testContent), fs.ModePerm) if err != nil { t.Fatalf("Error writing %s: %v", testFile, err) } - opts := Packer{AnnotationFilePath: testFile} + opts := Packer{ + AnnotationFilePath: testFile, + } - anno, err := opts.LoadManifestAnnotations() + err = opts.parseAnnotations(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - if !reflect.DeepEqual(anno, expectedResult) { - t.Fatalf("unexpected error: %v", anno) + if !reflect.DeepEqual(opts.Annotations, expectedResult) { + t.Fatalf("unexpected error: %v", opts.Annotations) } } -func TestPacker_LoadManifestAnnotations_annotationFlag(t *testing.T) { +func TestPacker_parseAnnotations_annotationFlag(t *testing.T) { // Item do not contains '=' invalidFlag0 := []string{ "Key", } - var annotations map[string]map[string]string - opts := Packer{ManifestAnnotations: invalidFlag0} - _, err := opts.LoadManifestAnnotations() + opts := Packer{ + Annotation: Annotation{ + ManifestAnnotations: invalidFlag0, + }, + } + err := opts.parseAnnotations(nil) if !errors.Is(err, errAnnotationFormat) { t.Fatalf("unexpected error: %v", err) } @@ -102,8 +113,12 @@ func TestPacker_LoadManifestAnnotations_annotationFlag(t *testing.T) { "Key=0", "Key=1", } - opts = Packer{ManifestAnnotations: invalidFlag1} - _, err = opts.LoadManifestAnnotations() + opts = Packer{ + Annotation: Annotation{ + ManifestAnnotations: invalidFlag1, + }, + } + err = opts.parseAnnotations(nil) if !errors.Is(err, errAnnotationDuplication) { t.Fatalf("unexpected error: %v", err) } @@ -114,15 +129,19 @@ func TestPacker_LoadManifestAnnotations_annotationFlag(t *testing.T) { "Key1=Val", // 2. Normal Item "Key2=${env:USERNAME}", // 3. Item contains variable eg. "${env:USERNAME}" } - opts = Packer{ManifestAnnotations: validFlag} - annotations, err = opts.LoadManifestAnnotations() + opts = Packer{ + Annotation: Annotation{ + ManifestAnnotations: validFlag, + }, + } + err = opts.parseAnnotations(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } - if _, ok := annotations["$manifest"]; !ok { + if _, ok := opts.Annotations["$manifest"]; !ok { t.Fatalf("unexpected error: failed when looking for '$manifest' in annotations") } - if !reflect.DeepEqual(annotations, + if !reflect.DeepEqual(opts.Annotations, map[string]map[string]string{ "$manifest": { "Key0": "", diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 37d2ddf98..1531927d5 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -125,11 +125,7 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder func runAttach(cmd *cobra.Command, opts *attachOptions) error { ctx, logger := command.GetLogger(cmd, &opts.Common) - annotations, err := opts.LoadManifestAnnotations() - if err != nil { - return err - } - if len(opts.FileRefs) == 0 && len(annotations[option.AnnotationManifest]) == 0 { + if len(opts.FileRefs) == 0 && len(opts.Annotations[option.AnnotationManifest]) == 0 { return &oerrors.Error{ Err: errors.New(`neither file nor annotation provided in the command`), Usage: fmt.Sprintf("%s %s", cmd.Parent().CommandPath(), cmd.Use), @@ -161,7 +157,7 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error { if err != nil { return err } - descs, err := loadFiles(ctx, store, annotations, opts.FileRefs, displayStatus) + descs, err := loadFiles(ctx, store, opts.Annotations, opts.FileRefs, displayStatus) if err != nil { return err } @@ -179,7 +175,7 @@ func runAttach(cmd *cobra.Command, opts *attachOptions) error { packOpts := oras.PackManifestOptions{ Subject: &subject, - ManifestAnnotations: annotations[option.AnnotationManifest], + ManifestAnnotations: opts.Annotations[option.AnnotationManifest], Layers: descs, } pack := func() (ocispec.Descriptor, error) { diff --git a/cmd/oras/root/attach_test.go b/cmd/oras/root/attach_test.go index 7bc666377..895bb30be 100644 --- a/cmd/oras/root/attach_test.go +++ b/cmd/oras/root/attach_test.go @@ -26,18 +26,20 @@ import ( ) func Test_runAttach_errType(t *testing.T) { - // prpare + // prepare cmd := &cobra.Command{} cmd.SetContext(context.Background()) // test opts := &attachOptions{ Packer: option.Packer{ - AnnotationFilePath: "/tmp/whatever", - ManifestAnnotations: []string{"one", "two"}, + Annotation: option.Annotation{ + ManifestAnnotations: []string{"one", "two"}, + }, + AnnotationFilePath: "/tmp/whatever", }, } - got := runAttach(cmd, opts).Error() + got := opts.Packer.Parse(cmd).Error() want := errors.New("`--annotation` and `--annotation-file` cannot be both specified").Error() if got != want { t.Fatalf("got %v, want %v", got, want) diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go index 1d6164d5d..12b51e95d 100644 --- a/cmd/oras/root/manifest/index/create.go +++ b/cmd/oras/root/manifest/index/create.go @@ -47,6 +47,7 @@ type createOptions struct { option.Common option.Target option.Pretty + option.Annotation sources []string extraRefs []string @@ -72,6 +73,9 @@ Example - Create an index from source manifests using both tags and digests, and Example - Create an index and push it with multiple tags: oras manifest index create localhost:5000/hello:tag1,tag2,tag3 linux-amd64 linux-arm64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 +Example - Create and push an index with annotations: + oras manifest index create localhost:5000/hello:v1 linux-amd64 --annotation "key=val" + Example - Create an index and push to an OCI image layout folder 'layout-dir' and tag with 'v1': oras manifest index create layout-dir:v1 linux-amd64 sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9 @@ -113,8 +117,9 @@ func createIndex(cmd *cobra.Command, opts createOptions) error { Versioned: specs.Versioned{ SchemaVersion: 2, }, - MediaType: ocispec.MediaTypeImageIndex, - Manifests: manifests, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: manifests, + Annotations: opts.Annotations[option.AnnotationManifest], } indexBytes, err := json.Marshal(index) if err != nil { diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index f8afa6a3c..92ad30e5e 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -157,15 +157,11 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t func runPush(cmd *cobra.Command, opts *pushOptions) error { ctx, logger := command.GetLogger(cmd, &opts.Common) - annotations, err := opts.LoadManifestAnnotations() - if err != nil { - return err - } // prepare pack packOpts := oras.PackManifestOptions{ - ConfigAnnotations: annotations[option.AnnotationConfig], - ManifestAnnotations: annotations[option.AnnotationManifest], + ConfigAnnotations: opts.Annotations[option.AnnotationConfig], + ManifestAnnotations: opts.Annotations[option.AnnotationManifest], } store, err := file.New("") if err != nil { @@ -190,7 +186,7 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { if err != nil { return err } - descs, err := loadFiles(ctx, store, annotations, opts.FileRefs, displayStatus) + descs, err := loadFiles(ctx, store, opts.Annotations, opts.FileRefs, displayStatus) if err != nil { return err } diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go index d6da1797a..0bcbd8a79 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -137,6 +137,19 @@ var _ = Describe("1.1 registry users:", func() { ValidateIndex(content, expectedManifests) }) + It("should create index with annotations", func() { + testRepo := indexTestRepo("create", "with-annotations") + key := "image-anno-key" + value := "image-anno-value" + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, "v1"), "--annotation", fmt.Sprintf("%s=%s", key, value)).Exec() + // verify + content := ORAS("manifest", "fetch", RegistryRef(ZOTHost, testRepo, "v1")).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(content, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Annotations[key]).To(Equal(value)) + }) + It("should output created index to file", func() { testRepo := indexTestRepo("create", "output-to-file") CopyZOTRepo(ImageRepo, testRepo) @@ -159,6 +172,14 @@ var _ = Describe("1.1 registry users:", func() { "does-not-exist").ExpectFailure(). MatchErrKeyWords("Error", "could not find", "does-not-exist").Exec() }) + + It("should fail if given annotation input of wrong format", func() { + testRepo := indexTestRepo("create", "bad-annotations") + CopyZOTRepo(ImageRepo, testRepo) + ORAS("manifest", "index", "create", RegistryRef(ZOTHost, testRepo, ""), + string(multi_arch.LinuxAMD64.Digest), "-a", "foo:bar").ExpectFailure(). + MatchErrKeyWords("Error", "annotation value doesn't match the required format").Exec() + }) }) When("running `manifest index update`", func() { @@ -405,6 +426,19 @@ var _ = Describe("OCI image layout users:", func() { ValidateIndex(content, expectedManifests) }) + It("should create index with annotations", func() { + root := PrepareTempOCI(ImageRepo) + indexRef := LayoutRef(root, "with-annotations") + key := "image-anno-key" + value := "image-anno-value" + ORAS("manifest", "index", "create", Flags.Layout, indexRef, "--annotation", fmt.Sprintf("%s=%s", key, value)).Exec() + // verify + content := ORAS("manifest", "fetch", Flags.Layout, indexRef).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(content, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Annotations[key]).To(Equal(value)) + }) + It("should output created index to file", func() { root := PrepareTempOCI(ImageRepo) indexRef := LayoutRef(root, "output-to-file") From e0f4066919bd4152dbbf518ab538d90bfb0e6145 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 26 Sep 2024 01:56:15 -0600 Subject: [PATCH 18/30] fix: Rename copy function to avoid built in collision (#1510) Signed-off-by: Terry Howe --- cmd/oras/root/push.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 92ad30e5e..4c7ff4d22 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -216,7 +216,7 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { copyOptions.CopyGraphOptions.OnCopySkipped = displayStatus.OnCopySkipped copyOptions.CopyGraphOptions.PreCopy = displayStatus.PreCopy copyOptions.CopyGraphOptions.PostCopy = displayStatus.PostCopy - copy := func(root ocispec.Descriptor) error { + copyWithScopeHint := func(root ocispec.Descriptor) error { // add both pull and push scope hints for dst repository // to save potential push-scope token requests during copy ctx = registryutil.WithScopeHint(ctx, dst, auth.ActionPull, auth.ActionPush) @@ -230,7 +230,7 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { } // Push - root, err := doPush(dst, stopTrack, pack, copy) + root, err := doPush(dst, stopTrack, pack, copyWithScopeHint) if err != nil { return err } @@ -272,7 +272,7 @@ func doPush(dst oras.Target, stopTrack status.StopTrackTargetFunc, pack packFunc type packFunc func() (ocispec.Descriptor, error) type copyFunc func(desc ocispec.Descriptor) error -func pushArtifact(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { +func pushArtifact(_ oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, error) { root, err := pack() if err != nil { return ocispec.Descriptor{}, err From 952d867fdf3117847c91606e21532abf22b42a44 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sun, 29 Sep 2024 19:38:20 -0600 Subject: [PATCH 19/30] refactor: create an interface for status console (#1481) Signed-off-by: Terry Howe --- .../display/status/console/console.go | 63 ++++++----- .../display/status/console/console_test.go | 103 +++++++++++++++--- .../display/status/progress/manager.go | 8 +- .../display/status/progress/manager_test.go | 9 +- internal/testutils/console.go | 8 +- 5 files changed, 138 insertions(+), 53 deletions(-) diff --git a/cmd/oras/internal/display/status/console/console.go b/cmd/oras/internal/display/status/console/console.go index 419c63a3d..fd9c06ec3 100644 --- a/cmd/oras/internal/display/status/console/console.go +++ b/cmd/oras/internal/display/status/console/console.go @@ -18,7 +18,7 @@ package console import ( "os" - "github.com/containerd/console" + containerd "github.com/containerd/console" "github.com/morikuni/aec" ) @@ -33,52 +33,59 @@ const ( Restore = "\0338" ) -// Console is a wrapper around containerd's console.Console and ANSI escape -// codes. -type Console struct { - console.Console +// Console is a wrapper around containerd's Console and ANSI escape codes. +type Console interface { + containerd.Console + GetHeightWidth() (height, width int) + Save() + NewRow() + OutputTo(upCnt uint, str string) + Restore() } -// Size returns the width and height of the console. -// If the console size cannot be determined, returns a default value of 80x10. -func (c *Console) Size() (width, height int) { - width = MinWidth - height = MinHeight - size, err := c.Console.Size() - if err == nil { - if size.Height > MinHeight { - height = int(size.Height) - } - if size.Width > MinWidth { - width = int(size.Width) - } - } - return +type console struct { + containerd.Console } -// New generates a Console from a file. -func New(f *os.File) (*Console, error) { - c, err := console.ConsoleFromFile(f) +// NewConsole generates a console from a file. +func NewConsole(f *os.File) (Console, error) { + c, err := containerd.ConsoleFromFile(f) if err != nil { return nil, err } - return &Console{c}, nil + return &console{c}, nil +} + +// GetHeightWidth returns the width and height of the console. +// If the console size cannot be determined, returns a default value of 80x10. +func (c *console) GetHeightWidth() (height, width int) { + windowSize, err := c.Console.Size() + if err != nil { + return MinHeight, MinWidth + } + if windowSize.Height < MinHeight { + windowSize.Height = MinHeight + } + if windowSize.Width < MinWidth { + windowSize.Width = MinWidth + } + return int(windowSize.Height), int(windowSize.Width) } // Save saves the current cursor position. -func (c *Console) Save() { +func (c *console) Save() { _, _ = c.Write([]byte(aec.Hide.Apply(Save))) } // NewRow allocates a horizontal space to the output area with scroll if needed. -func (c *Console) NewRow() { +func (c *console) NewRow() { _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte("\n")) _, _ = c.Write([]byte(Save)) } // OutputTo outputs a string to a specific line. -func (c *Console) OutputTo(upCnt uint, str string) { +func (c *console) OutputTo(upCnt uint, str string) { _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str))) _, _ = c.Write([]byte("\n")) @@ -86,7 +93,7 @@ func (c *Console) OutputTo(upCnt uint, str string) { } // Restore restores the saved cursor position. -func (c *Console) Restore() { +func (c *console) Restore() { // cannot use aec.Restore since DEC has better compatibility than SCO _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.Column(0). diff --git a/cmd/oras/internal/display/status/console/console_test.go b/cmd/oras/internal/display/status/console/console_test.go index 00e1f694c..65549e9e2 100644 --- a/cmd/oras/internal/display/status/console/console_test.go +++ b/cmd/oras/internal/display/status/console/console_test.go @@ -18,11 +18,38 @@ limitations under the License. package console import ( + "os" "testing" - "github.com/containerd/console" + containerd "github.com/containerd/console" + "oras.land/oras/internal/testutils" ) +func givenConsole(t *testing.T) (c Console, pty containerd.Console) { + pty, _, err := containerd.NewPty() + if err != nil { + t.Fatal(err) + } + + c = &console{ + Console: pty, + } + return c, pty +} + +func givenTestConsole(t *testing.T) (c Console, pty containerd.Console, tty *os.File) { + var err error + pty, tty, err = testutils.NewPty() + if err != nil { + t.Fatal(err) + } + + c = &console{ + Console: pty, + } + return c, pty, tty +} + func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) { t.Helper() if gotWidth != wantWidth { @@ -33,31 +60,77 @@ func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) } } -func TestConsole_Size(t *testing.T) { - pty, _, err := console.NewPty() - if err != nil { - t.Fatal(err) - } - c := &Console{ - Console: pty, +func TestNewConsole(t *testing.T) { + _, err := NewConsole(os.Stdin) + if err == nil { + t.Error("expected error creating bogus console") } +} + +func TestConsole_GetHeightWidth(t *testing.T) { + c, pty := givenConsole(t) // minimal width and height - gotWidth, gotHeight := c.Size() + gotHeight, gotWidth := c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero width - _ = pty.Resize(console.WinSize{Width: 0, Height: MinHeight}) - gotWidth, gotHeight = c.Size() + _ = pty.Resize(containerd.WinSize{Width: 0, Height: MinHeight}) + gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero height - _ = pty.Resize(console.WinSize{Width: MinWidth, Height: 0}) - gotWidth, gotHeight = c.Size() + _ = pty.Resize(containerd.WinSize{Width: MinWidth, Height: 0}) + gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // valid zero and height - _ = pty.Resize(console.WinSize{Width: 200, Height: 100}) - gotWidth, gotHeight = c.Size() + _ = pty.Resize(containerd.WinSize{Width: 200, Height: 100}) + gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, 200, 100) + +} + +func TestConsole_NewRow(t *testing.T) { + c, pty, tty := givenTestConsole(t) + + c.NewRow() + + err := testutils.MatchPty(pty, tty, "^[8\r\n^[7") + if err != nil { + t.Fatalf("NewRow output error: %v", err) + } +} + +func TestConsole_OutputTo(t *testing.T) { + c, pty, tty := givenTestConsole(t) + + c.OutputTo(1, "test string") + + err := testutils.MatchPty(pty, tty, "^[8^[[1Ftest string^[[0m\r\n^[[0K") + if err != nil { + t.Fatalf("OutputTo output error: %v", err) + } +} + +func TestConsole_Restore(t *testing.T) { + c, pty, tty := givenTestConsole(t) + + c.Restore() + + err := testutils.MatchPty(pty, tty, "^[8^[[0G^[[2K^[[?25h") + if err != nil { + t.Fatalf("Restore output error: %v", err) + } +} + +func TestConsole_Save(t *testing.T) { + c, pty, tty := givenTestConsole(t) + + c.Save() + + err := testutils.MatchPty(pty, tty, "^[[?25l^[7^[[0m") + if err != nil { + t.Fatalf("Save output error: %v", err) + } } diff --git a/cmd/oras/internal/display/status/progress/manager.go b/cmd/oras/internal/display/status/progress/manager.go index 2b526e47e..28e61d5e0 100644 --- a/cmd/oras/internal/display/status/progress/manager.go +++ b/cmd/oras/internal/display/status/progress/manager.go @@ -44,15 +44,15 @@ type Manager interface { type manager struct { status []*status statusLock sync.RWMutex - console *console.Console + console console.Console updating sync.WaitGroup renderDone chan struct{} renderClosed chan struct{} } // NewManager initialized a new progress manager. -func NewManager(f *os.File) (Manager, error) { - c, err := console.New(f) +func NewManager(tty *os.File) (Manager, error) { + c, err := console.NewConsole(tty) if err != nil { return nil, err } @@ -88,7 +88,7 @@ func (m *manager) render() { m.statusLock.RLock() defer m.statusLock.RUnlock() // todo: update size in another routine - width, height := m.console.Size() + height, width := m.console.GetHeightWidth() lineCount := len(m.status) * 2 offset := 0 if lineCount > height { diff --git a/cmd/oras/internal/display/status/progress/manager_test.go b/cmd/oras/internal/display/status/progress/manager_test.go index 01d2e4835..43d0f2104 100644 --- a/cmd/oras/internal/display/status/progress/manager_test.go +++ b/cmd/oras/internal/display/status/progress/manager_test.go @@ -31,10 +31,15 @@ func Test_manager_render(t *testing.T) { t.Fatal(err) } defer device.Close() + sole, err := console.NewConsole(device) + if err != nil { + t.Fatal(err) + } + m := &manager{ - console: &console.Console{Console: pty}, + console: sole, } - _, height := m.console.Size() + height, _ := m.console.GetHeightWidth() for i := 0; i < height; i++ { if _, err := m.Add(); err != nil { t.Fatal(err) diff --git a/internal/testutils/console.go b/internal/testutils/console.go index 8a262340b..5d26674c7 100644 --- a/internal/testutils/console.go +++ b/internal/testutils/console.go @@ -23,13 +23,13 @@ import ( "strings" "sync" - "github.com/containerd/console" + containerd "github.com/containerd/console" ) // NewPty creates a new pty pair for testing, caller is responsible for closing // the returned device file if err is not nil. -func NewPty() (console.Console, *os.File, error) { - pty, devicePath, err := console.NewPty() +func NewPty() (containerd.Console, *os.File, error) { + pty, devicePath, err := containerd.NewPty() if err != nil { return nil, nil, err } @@ -42,7 +42,7 @@ func NewPty() (console.Console, *os.File, error) { // MatchPty checks that the output matches the expected strings in specified // order. -func MatchPty(pty console.Console, device *os.File, expected ...string) error { +func MatchPty(pty containerd.Console, device *os.File, expected ...string) error { var wg sync.WaitGroup wg.Add(1) var buffer bytes.Buffer From 00a19d20644fe57d051d3b871579167dc2ff98e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:05:06 +0800 Subject: [PATCH 20/30] build(deps): bump golang.org/x/term from 0.24.0 to 0.25.0 (#1512) Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 46c9173ee..c7acf2c17 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 golang.org/x/sync v0.8.0 - golang.org/x/term v0.24.0 + golang.org/x/term v0.25.0 gopkg.in/yaml.v3 v3.0.1 oras.land/oras-go/v2 v2.5.0 ) @@ -29,5 +29,5 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect golang.org/x/crypto v0.26.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect ) diff --git a/go.sum b/go.sum index 467cc7dfc..c0001d698 100644 --- a/go.sum +++ b/go.sum @@ -60,10 +60,10 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 304a89fa124a9f2dc0296ccbd4bc0fe19a5c5e70 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sun, 13 Oct 2024 07:56:21 -0600 Subject: [PATCH 21/30] fix: document pushing files with colon in name (#1508) Signed-off-by: Terry Howe --- cmd/oras/root/push.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 4c7ff4d22..f9dfdee20 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -70,6 +70,9 @@ Example - Push file "hi.txt" with the custom media type "application/vnd.me.hi": Example - Push multiple files with different media types: oras push localhost:5000/hello:v1 hi.txt:application/vnd.me.hi bye.txt:application/vnd.me.bye +Example - Push file with colon in name "hi:txt" with the default media type: + oras push localhost:5000/hello:v1 hi:txt: + Example - Push file "hi.txt" with artifact type "application/vnd.example+type": oras push --artifact-type application/vnd.example+type localhost:5000/hello:v1 hi.txt From 1d60e226a5735f530d367dd9c56ce770562232e9 Mon Sep 17 00:00:00 2001 From: Xiaoxuan Wang <103478229+wangxiaoxuan273@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:47:14 +0800 Subject: [PATCH 22/30] fix: update --add should add plain descriptor (#1513) Signed-off-by: Xiaoxuan Wang --- cmd/oras/root/manifest/index/create.go | 20 ++++++++++++++------ cmd/oras/root/manifest/index/update.go | 7 ++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go index 12b51e95d..c8040559f 100644 --- a/cmd/oras/root/manifest/index/create.go +++ b/cmd/oras/root/manifest/index/create.go @@ -153,12 +153,8 @@ func fetchSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts return nil, fmt.Errorf("%s is not a manifest", source) } opts.Println(status.IndexPromptFetched, source) - desc = descriptor.Plain(desc) - if descriptor.IsImageManifest(desc) { - desc.Platform, err = getPlatform(ctx, target, content) - if err != nil { - return nil, err - } + if desc, err = enrichDescriptor(ctx, target, desc, content); err != nil { + return nil, err } resolved = append(resolved, desc) } @@ -209,3 +205,15 @@ func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, } return printer.Println("Digest:", desc.Digest) } + +func enrichDescriptor(ctx context.Context, target oras.ReadOnlyTarget, desc ocispec.Descriptor, manifestBytes []byte) (ocispec.Descriptor, error) { + desc = descriptor.Plain(desc) + if descriptor.IsImageManifest(desc) { + var err error + desc.Platform, err = getPlatform(ctx, target, manifestBytes) + if err != nil { + return ocispec.Descriptor{}, err + } + } + return desc, nil +} diff --git a/cmd/oras/root/manifest/index/update.go b/cmd/oras/root/manifest/index/update.go index d8a43b07c..7bff8a259 100644 --- a/cmd/oras/root/manifest/index/update.go +++ b/cmd/oras/root/manifest/index/update.go @@ -181,11 +181,8 @@ func addManifests(ctx context.Context, manifests []ocispec.Descriptor, target or return nil, fmt.Errorf("%s is not a manifest", manifestRef) } printUpdateStatus(status.IndexPromptFetched, manifestRef, string(desc.Digest), opts.Printer) - if descriptor.IsImageManifest(desc) { - desc.Platform, err = getPlatform(ctx, target, content) - if err != nil { - return nil, err - } + if desc, err = enrichDescriptor(ctx, target, desc, content); err != nil { + return nil, err } manifests = append(manifests, desc) printUpdateStatus(status.IndexPromptAdded, manifestRef, string(desc.Digest), opts.Printer) From 0fd725a5bb6f96f3a9d1f11537175bf17b463619 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 17 Oct 2024 04:34:53 -0600 Subject: [PATCH 23/30] feat: add platform option to push command (#1500) Signed-off-by: Terry Howe --- cmd/oras/internal/option/platform.go | 18 +++++- cmd/oras/root/push.go | 26 +++++++++ test/e2e/internal/testdata/foobar/const.go | 15 +++++ test/e2e/suite/command/push.go | 68 ++++++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/cmd/oras/internal/option/platform.go b/cmd/oras/internal/option/platform.go index 37cbea1d9..47dbf64b5 100644 --- a/cmd/oras/internal/option/platform.go +++ b/cmd/oras/internal/option/platform.go @@ -40,7 +40,7 @@ func (opts *Platform) ApplyFlags(fs *pflag.FlagSet) { fs.StringVarP(&opts.platform, "platform", "", "", opts.FlagDescription+" in the form of `os[/arch][/variant][:os_version]`") } -// parse parses the input platform flag to an oci platform type. +// Parse parses the input platform flag to an oci platform type. func (opts *Platform) Parse(*cobra.Command) error { if opts.platform == "" { return nil @@ -73,3 +73,19 @@ func (opts *Platform) Parse(*cobra.Command) error { opts.Platform = &p return nil } + +// ArtifactPlatform option struct. +type ArtifactPlatform struct { + Platform +} + +// ApplyFlags applies flags to a command flag set. +func (opts *ArtifactPlatform) ApplyFlags(fs *pflag.FlagSet) { + opts.FlagDescription = "set artifact platform" + fs.StringVarP(&opts.platform, "artifact-platform", "", "", "[Experimental] "+opts.FlagDescription+" in the form of `os[/arch][/variant][:os_version]`") +} + +// Parse parses the input platform flag to an oci platform type. +func (opts *ArtifactPlatform) Parse(cmd *cobra.Command) error { + return opts.Platform.Parse(cmd) +} diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index f9dfdee20..f053cec38 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -16,6 +16,8 @@ limitations under the License. package root import ( + "bytes" + "encoding/json" "errors" "strings" @@ -41,6 +43,7 @@ import ( type pushOptions struct { option.Common option.Packer + option.ArtifactPlatform option.ImageSpec option.Target option.Format @@ -100,6 +103,9 @@ Example - Push repository with manifest annotations: Example - Push repository with manifest annotation file: oras push --annotation-file annotation.json localhost:5000/hello:v1 +Example - Push artifact to repository with platform: + oras push --artifact-platform linux/arm/v5 localhost:5000/hello:v1 + Example - Push file "hi.txt" with multiple tags: oras push localhost:5000/hello:tag1,tag2,tag3 hi.txt @@ -133,6 +139,10 @@ Example - Push file "hi.txt" into an OCI image layout folder 'layout-dir' with t } } } + configAndPlatform := []string{"config", "artifact-platform"} + if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), configAndPlatform...); err != nil { + return err + } switch opts.PackVersion { case oras.PackManifestVersion1_0: @@ -182,6 +192,22 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { } desc.Annotations = packOpts.ConfigAnnotations packOpts.ConfigDescriptor = &desc + } else if opts.Platform.Platform != nil { + blob, err := json.Marshal(opts.Platform.Platform) + if err != nil { + return err + } + mediaType := oras.MediaTypeUnknownConfig + if opts.Flag == option.ImageSpecV1_0 && opts.artifactType != "" { + mediaType = opts.artifactType + } + desc := content.NewDescriptorFromBytes(mediaType, blob) + err = store.Push(ctx, desc, bytes.NewReader(blob)) + if err != nil { + return err + } + desc.Annotations = packOpts.ConfigAnnotations + packOpts.ConfigDescriptor = &desc } memoryStore := memory.New() union := contentutil.MultiReadOnlyTarget(memoryStore, store) diff --git a/test/e2e/internal/testdata/foobar/const.go b/test/e2e/internal/testdata/foobar/const.go index dc3de4a89..e4b39cfb2 100644 --- a/test/e2e/internal/testdata/foobar/const.go +++ b/test/e2e/internal/testdata/foobar/const.go @@ -38,6 +38,21 @@ var ( Digest: "46b68ac1696c", Name: "application/vnd.unknown.config.v1+json", } + PlatformConfigSize = 38 + PlatformConfigDigest = digest.Digest("sha256:e94c0ba80a1157ffab5b5c6656fffc089c6446c7ed0604f3382910d1ef7dd40d") + PlatformConfigStateKey = match.StateKey{ + Digest: "e94c0ba80a11", Name: "application/vnd.unknown.config.v1+json", + } + + PlatformV10ConfigSize = 38 + PlatformV10ConfigDigest = digest.Digest("sha256:e94c0ba80a1157ffab5b5c6656fffc089c6446c7ed0604f3382910d1ef7dd40d") + PlatformV10ConfigStateKey = match.StateKey{ + Digest: "e94c0ba80a11", Name: "test/artifact+json", + } + PlatformV1DEfaultConfigStateKey = match.StateKey{ + Digest: "e94c0ba80a11", Name: "application/vnd.unknown.config.v1+json", + } + FileBarName = "foobar/bar" FileBarStateKey = match.StateKey{Digest: "fcde2b2edba5", Name: FileLayerNames[2]} FileStateKeys = []match.StateKey{ diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index 5cb644a66..cfba901bc 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -77,6 +77,14 @@ var _ = Describe("ORAS beginners:", func() { ORAS("push", ref, "--config", foobar.FileConfigName, "--artifact-type", "test/artifact+json", "--image-spec", "v1.0").ExpectFailure().WithWorkDir(tempDir).Exec() }) + It("should fail to use --artifact-platform and --config at the same time", func() { + tempDir := PrepareTempFiles() + repo := pushTestRepo("no-mediatype") + ref := RegistryRef(ZOTHost, repo, "") + + ORAS("push", ref, "--artifact-platform", "linux/amd64", "--config", foobar.FileConfigName).ExpectFailure().WithWorkDir(tempDir).Exec() + }) + It("should fail if image spec is not valid", func() { testRepo := attachTestRepo("invalid-image-spec") subjectRef := RegistryRef(ZOTHost, testRepo, foobar.Tag) @@ -612,6 +620,66 @@ var _ = Describe("OCI image layout users:", func() { })) }) + It("should push files with platform", func() { + tempDir := PrepareTempFiles() + ref := LayoutRef(tempDir, tag) + ORAS("push", Flags.Layout, ref, "--artifact-platform", "darwin/arm64", foobar.FileBarName, "-v"). + MatchStatus([]match.StateKey{ + foobar.PlatformConfigStateKey, + foobar.FileBarStateKey, + }, true, 2). + WithWorkDir(tempDir).Exec() + // validate + fetched := ORAS("manifest", "fetch", Flags.Layout, ref).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Config).Should(Equal(ocispec.Descriptor{ + MediaType: foobar.PlatformConfigStateKey.Name, + Size: int64(foobar.PlatformConfigSize), + Digest: foobar.PlatformConfigDigest, + })) + }) + + It("should push files with platform with mediaType as artifactType for v1.0", func() { + tempDir := PrepareTempFiles() + ref := LayoutRef(tempDir, tag) + ORAS("push", Flags.Layout, ref, "--image-spec", "v1.0", "--artifact-type", "test/artifact+json", "--artifact-platform", "darwin/arm64", foobar.FileBarName, "-v"). + MatchStatus([]match.StateKey{ + foobar.PlatformV10ConfigStateKey, + foobar.FileBarStateKey, + }, true, 2). + WithWorkDir(tempDir).Exec() + // validate + fetched := ORAS("manifest", "fetch", Flags.Layout, ref).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Config).Should(Equal(ocispec.Descriptor{ + MediaType: "test/artifact+json", + Size: int64(foobar.PlatformV10ConfigSize), + Digest: foobar.PlatformV10ConfigDigest, + })) + }) + + It("should push files with platform with no artifactType for v1.0", func() { + tempDir := PrepareTempFiles() + ref := LayoutRef(tempDir, tag) + ORAS("push", Flags.Layout, ref, "--image-spec", "v1.0", "--artifact-platform", "darwin/arm64", foobar.FileBarName, "-v"). + MatchStatus([]match.StateKey{ + foobar.PlatformV1DEfaultConfigStateKey, + foobar.FileBarStateKey, + }, true, 2). + WithWorkDir(tempDir).Exec() + // validate + fetched := ORAS("manifest", "fetch", Flags.Layout, ref).Exec().Out.Contents() + var manifest ocispec.Manifest + Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred()) + Expect(manifest.Config).Should(Equal(ocispec.Descriptor{ + MediaType: "application/vnd.unknown.config.v1+json", + Size: int64(foobar.PlatformV10ConfigSize), + Digest: foobar.PlatformV10ConfigDigest, + })) + }) + It("should push files with customized manifest annotation", func() { tempDir := PrepareTempFiles() ref := LayoutRef(tempDir, tag) From 97cb3766ad2055ddbf3cb5dd49c2725ae0b12148 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 17 Oct 2024 10:17:32 -0600 Subject: [PATCH 24/30] chore: remove unneeded parse method (#1515) Signed-off-by: Terry Howe --- cmd/oras/internal/option/platform.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cmd/oras/internal/option/platform.go b/cmd/oras/internal/option/platform.go index 47dbf64b5..78c4fcbf5 100644 --- a/cmd/oras/internal/option/platform.go +++ b/cmd/oras/internal/option/platform.go @@ -84,8 +84,3 @@ func (opts *ArtifactPlatform) ApplyFlags(fs *pflag.FlagSet) { opts.FlagDescription = "set artifact platform" fs.StringVarP(&opts.platform, "artifact-platform", "", "", "[Experimental] "+opts.FlagDescription+" in the form of `os[/arch][/variant][:os_version]`") } - -// Parse parses the input platform flag to an oci platform type. -func (opts *ArtifactPlatform) Parse(cmd *cobra.Command) error { - return opts.Platform.Parse(cmd) -} From 270aa65a59113bacaf4057857aede8c4ae5695f7 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 29 Oct 2024 01:01:02 -0600 Subject: [PATCH 25/30] fix: platform config type (#1518) Signed-off-by: Terry Howe --- cmd/oras/root/push.go | 2 +- test/e2e/internal/testdata/foobar/const.go | 4 ++-- test/e2e/suite/command/push.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index f053cec38..323468d39 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -197,7 +197,7 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { if err != nil { return err } - mediaType := oras.MediaTypeUnknownConfig + mediaType := ocispec.MediaTypeImageConfig if opts.Flag == option.ImageSpecV1_0 && opts.artifactType != "" { mediaType = opts.artifactType } diff --git a/test/e2e/internal/testdata/foobar/const.go b/test/e2e/internal/testdata/foobar/const.go index e4b39cfb2..70af1eb3d 100644 --- a/test/e2e/internal/testdata/foobar/const.go +++ b/test/e2e/internal/testdata/foobar/const.go @@ -41,7 +41,7 @@ var ( PlatformConfigSize = 38 PlatformConfigDigest = digest.Digest("sha256:e94c0ba80a1157ffab5b5c6656fffc089c6446c7ed0604f3382910d1ef7dd40d") PlatformConfigStateKey = match.StateKey{ - Digest: "e94c0ba80a11", Name: "application/vnd.unknown.config.v1+json", + Digest: "e94c0ba80a11", Name: "application/vnd.oci.image.config.v1+json", } PlatformV10ConfigSize = 38 @@ -50,7 +50,7 @@ var ( Digest: "e94c0ba80a11", Name: "test/artifact+json", } PlatformV1DEfaultConfigStateKey = match.StateKey{ - Digest: "e94c0ba80a11", Name: "application/vnd.unknown.config.v1+json", + Digest: "e94c0ba80a11", Name: "application/vnd.oci.image.config.v1+json", } FileBarName = "foobar/bar" diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index cfba901bc..7dba0b3a2 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -674,7 +674,7 @@ var _ = Describe("OCI image layout users:", func() { var manifest ocispec.Manifest Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred()) Expect(manifest.Config).Should(Equal(ocispec.Descriptor{ - MediaType: "application/vnd.unknown.config.v1+json", + MediaType: "application/vnd.oci.image.config.v1+json", Size: int64(foobar.PlatformV10ConfigSize), Digest: foobar.PlatformV10ConfigDigest, })) From 4b6a6c275c208bbed4e1375f5fcd1af6554252bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 05:33:30 -0600 Subject: [PATCH 26/30] build(deps): bump github.com/onsi/ginkgo/v2 from 2.20.2 to 2.21.0 in /test/e2e (#1522) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/e2e/go.mod | 12 ++++++------ test/e2e/go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 71de7dd8e..508d014ac 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -3,7 +3,7 @@ module oras.land/oras/test/e2e go 1.23.0 require ( - github.com/onsi/ginkgo/v2 v2.20.2 + github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.34.2 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 @@ -15,11 +15,11 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect - golang.org/x/net v0.28.0 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 5044da73f..ebd8a7163 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -6,10 +6,10 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -20,16 +20,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From 0f62d457fd589fbe88c326034a6864dc48cfe7ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 05:38:04 -0600 Subject: [PATCH 27/30] build(deps): bump github.com/onsi/gomega from 1.34.2 to 1.35.0 in /test/e2e (#1521) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/e2e/go.mod | 2 +- test/e2e/go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 508d014ac..09343c566 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/onsi/ginkgo/v2 v2.21.0 - github.com/onsi/gomega v1.34.2 + github.com/onsi/gomega v1.35.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/test/e2e/go.sum b/test/e2e/go.sum index ebd8a7163..153849151 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -10,8 +10,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/gomega v1.35.0 h1:xuM1M/UvMp9BCdS4hojhS9/4jEuVqS9Er3bqupeaoPM= +github.com/onsi/gomega v1.35.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -30,8 +30,8 @@ golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= From b6ac8e987f0c6be1e966c3c9ad42fd129498a4fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 06:08:15 -0600 Subject: [PATCH 28/30] build(deps): bump github.com/onsi/gomega from 1.35.0 to 1.35.1 in /test/e2e (#1523) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/e2e/go.mod | 2 +- test/e2e/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 09343c566..8a23ac1d2 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( github.com/onsi/ginkgo/v2 v2.21.0 - github.com/onsi/gomega v1.35.0 + github.com/onsi/gomega v1.35.1 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 153849151..c12d67b5d 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -10,8 +10,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.0 h1:xuM1M/UvMp9BCdS4hojhS9/4jEuVqS9Er3bqupeaoPM= -github.com/onsi/gomega v1.35.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= From 9a83394134aca2b2473a687e4906abc34d5406a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauricio=20V=C3=A1squez?= Date: Sun, 3 Nov 2024 08:07:19 -0500 Subject: [PATCH 29/30] feat(cmd/cp): add oci-layout-path flag (#1507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mauricio Vásquez Co-authored-by: Terry Howe --- cmd/oras/internal/option/target.go | 9 +++ cmd/oras/internal/option/target_test.go | 42 +++++++++++- test/e2e/internal/utils/const.go | 4 ++ test/e2e/suite/command/cp.go | 87 ++++++++++++++++++++++++- 4 files changed, 139 insertions(+), 3 deletions(-) diff --git a/cmd/oras/internal/option/target.go b/cmd/oras/internal/option/target.go index 489eeb820..839f376db 100644 --- a/cmd/oras/internal/option/target.go +++ b/cmd/oras/internal/option/target.go @@ -85,6 +85,7 @@ func (opts *Target) AnnotatedReference() string { func (opts *Target) applyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) { flagPrefix, notePrefix := applyPrefix(prefix, description) fs.BoolVarP(&opts.IsOCILayout, flagPrefix+"oci-layout", "", false, "set "+notePrefix+"target as an OCI image layout") + fs.StringVar(&opts.Path, flagPrefix+"oci-layout-path", "", "set the path for the "+notePrefix+"OCI image layout target") } // ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string. @@ -96,6 +97,10 @@ func (opts *Target) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description // Parse gets target options from user input. func (opts *Target) Parse(cmd *cobra.Command) error { + if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), opts.flagPrefix+"oci-layout-path", opts.flagPrefix+"oci-layout"); err != nil { + return err + } + switch { case opts.IsOCILayout: opts.Type = TargetTypeOCILayout @@ -103,6 +108,10 @@ func (opts *Target) Parse(cmd *cobra.Command) error { return errors.New("custom header flags cannot be used on an OCI image layout target") } return opts.parseOCILayoutReference() + case opts.Path != "": + opts.Type = TargetTypeOCILayout + opts.Reference = opts.RawReference + return nil default: opts.Type = TargetTypeRemote if ref, err := registry.ParseReference(opts.RawReference); err != nil { diff --git a/cmd/oras/internal/option/target_test.go b/cmd/oras/internal/option/target_test.go index 9a227b768..52931bdf9 100644 --- a/cmd/oras/internal/option/target_test.go +++ b/cmd/oras/internal/option/target_test.go @@ -20,6 +20,7 @@ import ( "net/http" "net/url" "reflect" + "strings" "testing" "github.com/spf13/cobra" @@ -28,9 +29,26 @@ import ( oerrors "oras.land/oras/cmd/oras/internal/errors" ) +func TestTarget_Parse_oci_path(t *testing.T) { + opts := Target{ + Path: "foo", + RawReference: "mocked/test", + } + cmd := &cobra.Command{} + ApplyFlags(&opts, cmd.Flags()) + if err := opts.Parse(cmd); err != nil { + t.Errorf("Target.Parse() error = %v", err) + } + if opts.Type != TargetTypeOCILayout { + t.Errorf("Target.Parse() failed, got %q, want %q", opts.Type, TargetTypeOCILayout) + } +} + func TestTarget_Parse_oci(t *testing.T) { opts := Target{IsOCILayout: true} - err := opts.Parse(nil) + cmd := &cobra.Command{} + ApplyFlags(&opts, cmd.Flags()) + err := opts.Parse(cmd) if !errors.Is(err, errdef.ErrInvalidReference) { t.Errorf("Target.Parse() error = %v, expect %v", err, errdef.ErrInvalidReference) } @@ -39,6 +57,24 @@ func TestTarget_Parse_oci(t *testing.T) { } } +func TestTarget_Parse_oci_and_oci_path(t *testing.T) { + opts := Target{} + cmd := &cobra.Command{} + opts.ApplyFlags(cmd.Flags()) + cmd.SetArgs([]string{"--oci-layout", "foo", "--oci-layout-path", "foo"}) + if err := cmd.Execute(); err != nil { + t.Errorf("cmd.Execute() error = %v", err) + } + err := opts.Parse(cmd) + if err == nil { + t.Errorf("expect Target.Parse() to fail but not") + } + if !strings.Contains(err.Error(), "cannot be used at the same time") { + t.Errorf("expect error message to contain 'cannot be used at the same time' but not") + } + +} + func TestTarget_Parse_remote(t *testing.T) { opts := Target{ RawReference: "mocked/test", @@ -59,7 +95,9 @@ func TestTarget_Parse_remote_err(t *testing.T) { RawReference: "/test", IsOCILayout: false, } - if err := opts.Parse(nil); err == nil { + cmd := &cobra.Command{} + ApplyFlags(&opts, cmd.Flags()) + if err := opts.Parse(cmd); err == nil { t.Errorf("expect Target.Parse() to fail but not") } } diff --git a/test/e2e/internal/utils/const.go b/test/e2e/internal/utils/const.go index 6c247aa29..4ea0940fd 100644 --- a/test/e2e/internal/utils/const.go +++ b/test/e2e/internal/utils/const.go @@ -20,12 +20,16 @@ var ( Layout string FromLayout string ToLayout string + FromLayoutPath string + ToLayoutPath string DistributionSpec string ImageSpec string }{ "--oci-layout", "--from-oci-layout", "--to-oci-layout", + "--from-oci-layout-path", + "--to-oci-layout-path", "--distribution-spec", "--image-spec", } diff --git a/test/e2e/suite/command/cp.go b/test/e2e/suite/command/cp.go index f548dd0be..70b278f64 100644 --- a/test/e2e/suite/command/cp.go +++ b/test/e2e/suite/command/cp.go @@ -441,7 +441,7 @@ var _ = Describe("OCI layout users:", func() { It("should copy an image from a registry to an OCI image layout via digest", func() { dstDir := GinkgoT().TempDir() - src := RegistryRef(ZOTHost, ImageRepo, foobar.Tag) + src := RegistryRef(ZOTHost, ImageRepo, foobar.Digest) ORAS("cp", src, dstDir, "-v", Flags.ToLayout).MatchStatus(foobarStates, true, len(foobarStates)).Exec() // validate srcManifest := ORAS("manifest", "fetch", src).WithDescription("fetch from source to validate").Exec().Out.Contents() @@ -625,6 +625,91 @@ var _ = Describe("OCI layout users:", func() { Expect(len(index.Manifests)).To(Equal(1)) Expect(index.Manifests[0].Digest.String()).To(Equal(ma.LinuxAMD64Referrer.Digest.String())) }) + + // oci-layout-path tests + + It("should copy an image from a registry to an OCI image layout via tag using --oci-layout-path", func() { + layoutDir := GinkgoT().TempDir() + src := RegistryRef(ZOTHost, ImageRepo, foobar.Tag) + ref := "copied" + dst := LayoutRef(layoutDir, ref) + ORAS("cp", src, ref, "-v", Flags.ToLayoutPath, layoutDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst, Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image from an OCI image layout to a registry via tag using --oci-layout-path", func() { + layoutDir := GinkgoT().TempDir() + ref := "copied" + src := LayoutRef(layoutDir, ref) + dst := RegistryRef(ZOTHost, cpTestRepo("from-layout-tag-path"), foobar.Tag) + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), src, Flags.ToLayout).Exec() + // test + ORAS("cp", ref, dst, "-v", Flags.FromLayoutPath, layoutDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image between OCI image layouts via tag using --oci-layout-path", func() { + srcDir := GinkgoT().TempDir() + toDir := GinkgoT().TempDir() + srcRef := "from" + dstRef := "to" + src := LayoutRef(srcDir, srcRef) + dst := LayoutRef(toDir, dstRef) + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), src, Flags.ToLayout).Exec() + // test + ORAS("cp", srcRef, dstRef, "-v", Flags.FromLayoutPath, srcDir, Flags.ToLayoutPath, toDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst, Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image from a registry to an OCI image layout via digest using --oci-layout-path", func() { + dstDir := GinkgoT().TempDir() + src := RegistryRef(ZOTHost, ImageRepo, foobar.Digest) + ORAS("cp", src, foobar.Digest, "-v", Flags.ToLayoutPath, dstDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", LayoutRef(dstDir, foobar.Digest), Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image from an OCI image layout to a registry via digest using --oci-layout-path", func() { + layoutDir := GinkgoT().TempDir() + src := LayoutRef(layoutDir, foobar.Digest) + dst := RegistryRef(ZOTHost, cpTestRepo("from-layout-digest-path"), "copied") + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), layoutDir, Flags.ToLayout).Exec() + // test + ORAS("cp", foobar.Digest, dst, "-v", Flags.FromLayoutPath, layoutDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) + + It("should copy an image between OCI image layouts via digest", func() { + srcDir := GinkgoT().TempDir() + toDir := GinkgoT().TempDir() + src := LayoutRef(srcDir, foobar.Digest) + dst := LayoutRef(toDir, foobar.Digest) + // prepare + ORAS("cp", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), srcDir, Flags.ToLayout).Exec() + // test + ORAS("cp", foobar.Digest, foobar.Digest, "-v", Flags.FromLayoutPath, srcDir, Flags.ToLayoutPath, toDir).MatchStatus(foobarStates, true, len(foobarStates)).Exec() + // validate + srcManifest := ORAS("manifest", "fetch", src, Flags.Layout).WithDescription("fetch from source to validate").Exec().Out.Contents() + dstManifest := ORAS("manifest", "fetch", dst, Flags.Layout).WithDescription("fetch from destination to validate").Exec().Out.Contents() + Expect(srcManifest).To(Equal(dstManifest)) + }) }) }) From 8f0f0cd12d59ae30cb1dc17daf99ae40923d6389 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Sun, 3 Nov 2024 21:09:53 +0800 Subject: [PATCH 30/30] fix: force config media type when setting platform (#1519) Signed-off-by: Billy Zha Co-authored-by: Terry Howe --- cmd/oras/root/push.go | 8 +++++--- test/e2e/suite/command/push.go | 18 +++--------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 323468d39..b5ae07e14 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -197,11 +197,13 @@ func runPush(cmd *cobra.Command, opts *pushOptions) error { if err != nil { return err } - mediaType := ocispec.MediaTypeImageConfig if opts.Flag == option.ImageSpecV1_0 && opts.artifactType != "" { - mediaType = opts.artifactType + return &oerrors.Error{ + Err: errors.New(`artifact type cannot be customized for OCI image-spec v1.0 when platform is specified`), + Recommendation: "consider using image spec v1.1 or remove --artifact-type", + } } - desc := content.NewDescriptorFromBytes(mediaType, blob) + desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageConfig, blob) err = store.Push(ctx, desc, bytes.NewReader(blob)) if err != nil { return err diff --git a/test/e2e/suite/command/push.go b/test/e2e/suite/command/push.go index 7dba0b3a2..516a56629 100644 --- a/test/e2e/suite/command/push.go +++ b/test/e2e/suite/command/push.go @@ -640,24 +640,12 @@ var _ = Describe("OCI image layout users:", func() { })) }) - It("should push files with platform with mediaType as artifactType for v1.0", func() { + It("should fail to customize config mediaType when baking config blob with platform for v1.0", func() { tempDir := PrepareTempFiles() ref := LayoutRef(tempDir, tag) ORAS("push", Flags.Layout, ref, "--image-spec", "v1.0", "--artifact-type", "test/artifact+json", "--artifact-platform", "darwin/arm64", foobar.FileBarName, "-v"). - MatchStatus([]match.StateKey{ - foobar.PlatformV10ConfigStateKey, - foobar.FileBarStateKey, - }, true, 2). - WithWorkDir(tempDir).Exec() - // validate - fetched := ORAS("manifest", "fetch", Flags.Layout, ref).Exec().Out.Contents() - var manifest ocispec.Manifest - Expect(json.Unmarshal(fetched, &manifest)).ShouldNot(HaveOccurred()) - Expect(manifest.Config).Should(Equal(ocispec.Descriptor{ - MediaType: "test/artifact+json", - Size: int64(foobar.PlatformV10ConfigSize), - Digest: foobar.PlatformV10ConfigDigest, - })) + ExpectFailure(). + Exec() }) It("should push files with platform with no artifactType for v1.0", func() {