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/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=./... ./... 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..79662a72f 100644 --- a/cmd/oras/internal/display/status/utils.go +++ b/cmd/oras/internal/display/status/utils.go @@ -48,6 +48,18 @@ const ( copyPromptMounted = "Mounted" ) +// Prompts for index events. +const ( + IndexPromptFetching = "Fetching " + IndexPromptFetched = "Fetched " + IndexPromptAdded = "Added " + IndexPromptMerged = "Merged " + IndexPromptRemoved = "Removed " + IndexPromptPacked = "Packed " + IndexPromptPushed = "Pushed " + IndexPromptUpdated = "Updated " +) + // 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/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/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/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 89a570210..1531927d5 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 @@ -119,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), @@ -155,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 } @@ -173,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/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 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/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/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/manifest/index/cmd.go b/cmd/oras/root/manifest/index/cmd.go new file mode 100644 index 000000000..3633faa03 --- /dev/null +++ b/cmd/oras/root/manifest/index/cmd.go @@ -0,0 +1,31 @@ +/* +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(), + updateCmd(), + ) + 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..12b51e95d --- /dev/null +++ b/cmd/oras/root/manifest/index/create.go @@ -0,0 +1,211 @@ +/* +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" + "os" + "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/contentutil" + "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 + option.Pretty + option.Annotation + + sources []string + extraRefs []string + outputPath 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 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 + +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 { + 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) + }, + } + 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) +} + +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, + Annotations: opts.Annotations[option.AnnotationManifest], + } + 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) + + 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) { + 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 == "" || contentutil.IsDigest(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/cmd/oras/root/manifest/index/update.go b/cmd/oras/root/manifest/index/update.go new file mode 100644 index 000000000..d8a43b07c --- /dev/null +++ b/cmd/oras/root/manifest/index/update.go @@ -0,0 +1,261 @@ +/* +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" + "os" + + "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 + option.Pretty + + addArguments []string + mergeArguments []string + removeArguments []string + tags []string + outputPath 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" + +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) { + 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") + cmd.Flags().StringVarP(&opts.outputPath, "output", "o", "", "file `path` to write the created index to, use - for stdout") + 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) + 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) { + 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/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..4c7ff4d22 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 @@ -151,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 { @@ -184,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 } @@ -214,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) @@ -228,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 } @@ -270,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 diff --git a/go.mod b/go.mod index 2b8fdb23a..46c9173ee 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module oras.land/oras -go 1.22.0 +go 1.23.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 @@ -12,22 +12,22 @@ 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 ) 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 - golang.org/x/sys v0.23.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.25.0 // indirect ) diff --git a/go.sum b/go.sum index e8d5662ee..467cc7dfc 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= +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.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= diff --git a/internal/descriptor/descriptor.go b/internal/descriptor/descriptor.go index 84806fed2..41f7321b7 100644 --- a/internal/descriptor/descriptor.go +++ b/internal/descriptor/descriptor.go @@ -21,11 +21,30 @@ 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 } +// 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() @@ -37,6 +56,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/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/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/go.mod b/test/e2e/go.mod index e7b96aeef..71de7dd8e 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -1,10 +1,10 @@ module oras.land/oras/test/e2e -go 1.22.0 +go 1.23.0 require ( - github.com/onsi/ginkgo/v2 v2.20.1 - github.com/onsi/gomega v1.34.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 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..5044da73f 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/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/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/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= 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 ( . diff --git a/test/e2e/internal/testdata/multi_arch/const.go b/test/e2e/internal/testdata/multi_arch/const.go index d4a3ae380..f8a1f3d8b 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,28 @@ 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", + }, + } +) + +// exported index +var ( + 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/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..0bcbd8a79 --- /dev/null +++ b/test/e2e/suite/command/manifest_index.go @@ -0,0 +1,589 @@ +/* +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" + "path/filepath" + + . "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() + }) + }) + }) + + 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 { + 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 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) + 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.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.OutputIndex).Exec() + }) + + 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() + }) + + 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() { + 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 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) + 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() + }) + + 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() + }) + }) +}) + +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 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") + 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.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.OutputIndex).Exec() + }) + + 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() + }) + }) + + 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 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") + 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() + }) + }) +}) 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() 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