diff --git a/cmd/oras/internal/display/content/discard.go b/cmd/oras/internal/display/content/discard.go index cdf543797..4d3f974d8 100644 --- a/cmd/oras/internal/display/content/discard.go +++ b/cmd/oras/internal/display/content/discard.go @@ -17,14 +17,19 @@ package content import ocispec "github.com/opencontainers/image-spec/specs-go/v1" -type discardHandler struct{} +type DiscardHandler struct{} // OnContentFetched implements ManifestFetchHandler. -func (discardHandler) OnContentFetched(ocispec.Descriptor, []byte) error { +func (DiscardHandler) OnContentFetched(ocispec.Descriptor, []byte) error { + return nil +} + +// OnContentCreated implements ManifestIndexCreateHandler. +func (DiscardHandler) OnContentCreated([]byte) error { return nil } // NewDiscardHandler returns a new discard handler. -func NewDiscardHandler() ManifestFetchHandler { - return discardHandler{} +func NewDiscardHandler() DiscardHandler { + return DiscardHandler{} } diff --git a/cmd/oras/internal/display/content/interface.go b/cmd/oras/internal/display/content/interface.go index 2c35fc552..9642aa82d 100644 --- a/cmd/oras/internal/display/content/interface.go +++ b/cmd/oras/internal/display/content/interface.go @@ -24,3 +24,12 @@ type ManifestFetchHandler interface { // OnContentFetched is called after the manifest content is fetched. OnContentFetched(desc ocispec.Descriptor, content []byte) error } + +// ManifestIndexCreateHandler handles raw output for manifest index create events. +type ManifestIndexCreateHandler interface { + // OnContentCreated is called after the index content is created. + OnContentCreated(content []byte) error +} + +// ManifestIndexUpdateHandler handles raw output for manifest index update events. +type ManifestIndexUpdateHandler ManifestIndexCreateHandler diff --git a/cmd/oras/internal/display/content/manifest_fetch.go b/cmd/oras/internal/display/content/manifest_fetch.go index d3a103092..9dcf7347b 100644 --- a/cmd/oras/internal/display/content/manifest_fetch.go +++ b/cmd/oras/internal/display/content/manifest_fetch.go @@ -46,6 +46,10 @@ func (h *manifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byt // NewManifestFetchHandler creates a new handler. func NewManifestFetchHandler(out io.Writer, pretty bool, outputPath string) ManifestFetchHandler { + // ignore --pretty when output to a file + if outputPath != "" && outputPath != "-" { + pretty = false + } return &manifestFetch{ pretty: pretty, stdout: out, diff --git a/cmd/oras/internal/display/content/manifest_index.go b/cmd/oras/internal/display/content/manifest_index.go new file mode 100644 index 000000000..b040aa998 --- /dev/null +++ b/cmd/oras/internal/display/content/manifest_index.go @@ -0,0 +1,58 @@ +/* +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 content + +import ( + "fmt" + "io" + "os" + + "oras.land/oras/cmd/oras/internal/output" +) + +// manifestIndexCreate handles raw content output. +type manifestIndexCreate struct { + pretty bool + stdout io.Writer + outputPath string +} + +// NewManifestIndexCreateHandler creates a new handler. +func NewManifestIndexCreateHandler(out io.Writer, pretty bool, outputPath string) ManifestIndexCreateHandler { + // ignore --pretty when output to a file + if outputPath != "" && outputPath != "-" { + pretty = false + } + return &manifestIndexCreate{ + pretty: pretty, + stdout: out, + outputPath: outputPath, + } +} + +// OnContentCreated is called after index content is created. +func (h *manifestIndexCreate) OnContentCreated(manifest []byte) error { + out := h.stdout + if h.outputPath != "" && h.outputPath != "-" { + f, err := os.Create(h.outputPath) + if err != nil { + return fmt.Errorf("failed to open %q: %w", h.outputPath, err) + } + defer f.Close() + out = f + } + return output.PrintJSON(out, manifest, h.pretty) +} diff --git a/cmd/oras/internal/display/content/manifest_index_test.go b/cmd/oras/internal/display/content/manifest_index_test.go new file mode 100644 index 000000000..0b943ea4a --- /dev/null +++ b/cmd/oras/internal/display/content/manifest_index_test.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 content + +import ( + "os" + "testing" +) + +func Test_manifestIndexCreate_OnContentCreated(t *testing.T) { + testHandler := NewManifestIndexCreateHandler(os.Stdout, false, "invalid/path") + if err := testHandler.OnContentCreated([]byte("test content")); err == nil { + t.Errorf("manifestIndexCreate.OnContentCreated() error = %v, wantErr non-nil error", err) + } +} diff --git a/cmd/oras/internal/display/handler.go b/cmd/oras/internal/display/handler.go index ccad542aa..de84a1723 100644 --- a/cmd/oras/internal/display/handler.go +++ b/cmd/oras/internal/display/handler.go @@ -174,9 +174,44 @@ 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) +// NewManifestIndexCreateHandler returns status, metadata and content handlers for index create command. +func NewManifestIndexCreateHandler(outputPath string, printer *output.Printer, pretty bool) (status.ManifestIndexCreateHandler, metadata.ManifestIndexCreateHandler, content.ManifestIndexCreateHandler) { + var statusHandler status.ManifestIndexCreateHandler + var metadataHandler metadata.ManifestIndexCreateHandler + var contentHandler content.ManifestIndexCreateHandler + switch outputPath { + case "": + statusHandler = status.NewTextManifestIndexCreateHandler(printer) + metadataHandler = text.NewManifestIndexCreateHandler(printer) + contentHandler = content.NewDiscardHandler() + case "-": + statusHandler = status.NewDiscardHandler() + metadataHandler = metadata.NewDiscardHandler() + contentHandler = content.NewManifestIndexCreateHandler(printer, pretty, outputPath) + default: + statusHandler = status.NewTextManifestIndexCreateHandler(printer) + metadataHandler = text.NewManifestIndexCreateHandler(printer) + contentHandler = content.NewManifestIndexCreateHandler(printer, pretty, outputPath) + } + return statusHandler, metadataHandler, contentHandler +} + +// NewManifestIndexUpdateHandler returns status, metadata and content handlers for index update command. +func NewManifestIndexUpdateHandler(outputPath string, printer *output.Printer, pretty bool) ( + status.ManifestIndexUpdateHandler, + metadata.ManifestIndexUpdateHandler, + content.ManifestIndexUpdateHandler) { + statusHandler := status.NewTextManifestIndexUpdateHandler(printer) + metadataHandler := text.NewManifestIndexCreateHandler(printer) + contentHandler := content.NewManifestIndexCreateHandler(printer, pretty, outputPath) + switch outputPath { + case "": + contentHandler = content.NewDiscardHandler() + case "-": + statusHandler = status.NewDiscardHandler() + metadataHandler = metadata.NewDiscardHandler() + } + return statusHandler, metadataHandler, contentHandler } // NewCopyHandler returns copy handlers. diff --git a/cmd/oras/internal/display/metadata/discard.go b/cmd/oras/internal/display/metadata/discard.go index 52c203eb2..d5a3d3e6d 100644 --- a/cmd/oras/internal/display/metadata/discard.go +++ b/cmd/oras/internal/display/metadata/discard.go @@ -15,16 +15,28 @@ limitations under the License. package metadata -import ocispec "github.com/opencontainers/image-spec/specs-go/v1" +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) -type discard struct{} +type Discard struct{} // NewDiscardHandler creates a new handler that discards output for all events. -func NewDiscardHandler() discard { - return discard{} +func NewDiscardHandler() Discard { + return Discard{} } // OnFetched implements ManifestFetchHandler. -func (discard) OnFetched(string, ocispec.Descriptor, []byte) error { +func (Discard) OnFetched(string, ocispec.Descriptor, []byte) error { + return nil +} + +// OnTagged implements ManifestIndexCreateHandler. +func (Discard) OnTagged(ocispec.Descriptor, string) error { + return nil +} + +// OnCompleted implements ManifestIndexCreateHandler. +func (Discard) OnCompleted(ocispec.Descriptor) error { return nil } diff --git a/cmd/oras/internal/display/metadata/discard_test.go b/cmd/oras/internal/display/metadata/discard_test.go new file mode 100644 index 000000000..9e9e67a8f --- /dev/null +++ b/cmd/oras/internal/display/metadata/discard_test.go @@ -0,0 +1,29 @@ +/* +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 metadata + +import ( + "testing" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestDiscard_OnTagged(t *testing.T) { + testDiscard := NewDiscardHandler() + if err := testDiscard.OnTagged(ocispec.Descriptor{}, "test"); err != nil { + t.Errorf("testDiscard.OnTagged() error = %v, want nil", err) + } +} diff --git a/cmd/oras/internal/display/metadata/interface.go b/cmd/oras/internal/display/metadata/interface.go index 12c10b87b..32b18f2bf 100644 --- a/cmd/oras/internal/display/metadata/interface.go +++ b/cmd/oras/internal/display/metadata/interface.go @@ -81,8 +81,12 @@ type ManifestPushHandler interface { // ManifestIndexCreateHandler handles metadata output for index create events. type ManifestIndexCreateHandler interface { TaggedHandler + OnCompleted(desc ocispec.Descriptor) error } +// ManifestIndexUpdateHandler handles metadata output for index update events. +type ManifestIndexUpdateHandler ManifestIndexCreateHandler + // 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.go similarity index 84% rename from cmd/oras/internal/display/metadata/text/manifest_index_create.go rename to cmd/oras/internal/display/metadata/text/manifest_index.go index 960f676c2..4550b7052 100644 --- a/cmd/oras/internal/display/metadata/text/manifest_index_create.go +++ b/cmd/oras/internal/display/metadata/text/manifest_index.go @@ -33,7 +33,12 @@ func NewManifestIndexCreateHandler(printer *output.Printer) metadata.ManifestInd } } -// OnTagged implements metadata.TaggedHandler. +// OnTagged implements TaggedHandler. func (h *ManifestIndexCreateHandler) OnTagged(_ ocispec.Descriptor, tag string) error { return h.printer.Println("Tagged", tag) } + +// OnCompleted implements ManifestIndexCreateHandler. +func (h *ManifestIndexCreateHandler) OnCompleted(desc ocispec.Descriptor) error { + return h.printer.Println("Digest:", desc.Digest) +} diff --git a/cmd/oras/internal/display/status/discard.go b/cmd/oras/internal/display/status/discard.go index 50b45b8f3..2bdc09ff4 100644 --- a/cmd/oras/internal/display/status/discard.go +++ b/cmd/oras/internal/display/status/discard.go @@ -18,6 +18,7 @@ package status import ( "context" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" ) @@ -88,3 +89,38 @@ func (DiscardHandler) OnNodeProcessing(desc ocispec.Descriptor) error { func (DiscardHandler) OnNodeSkipped(desc ocispec.Descriptor) error { return nil } + +// OnFetching implements referenceFetchHandler. +func (DiscardHandler) OnFetching(string) error { + return nil +} + +// OnFetched implements referenceFetchHandler. +func (DiscardHandler) OnFetched(string, ocispec.Descriptor) error { + return nil +} + +// OnManifestRemoved implements ManifestIndexUpdateHandler. +func (DiscardHandler) OnManifestRemoved(digest.Digest) error { + return nil +} + +// OnManifestAdded implements ManifestIndexUpdateHandler. +func (DiscardHandler) OnManifestAdded(string, ocispec.Descriptor) error { + return nil +} + +// OnIndexMerged implements ManifestIndexUpdateHandler. +func (DiscardHandler) OnIndexMerged(string, ocispec.Descriptor) error { + return nil +} + +// OnIndexPacked implements ManifestIndexCreateHandler. +func (DiscardHandler) OnIndexPacked(ocispec.Descriptor) error { + return nil +} + +// OnIndexPushed implements ManifestIndexCreateHandler. +func (DiscardHandler) OnIndexPushed(string) error { + return nil +} diff --git a/cmd/oras/internal/display/status/discard_test.go b/cmd/oras/internal/display/status/discard_test.go new file mode 100644 index 000000000..bf5a71363 --- /dev/null +++ b/cmd/oras/internal/display/status/discard_test.go @@ -0,0 +1,43 @@ +/* +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 status + +import ( + "testing" + + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestDiscardHandler_OnManifestRemoved(t *testing.T) { + testDiscard := NewDiscardHandler() + if err := testDiscard.OnManifestRemoved("sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"); err != nil { + t.Errorf("DiscardHandler.OnManifestRemoved() error = %v, wantErr nil", err) + } +} + +func TestDiscardHandler_OnIndexMerged(t *testing.T) { + testDiscard := NewDiscardHandler() + if err := testDiscard.OnIndexMerged("test", v1.Descriptor{}); err != nil { + t.Errorf("DiscardHandler.OnIndexMerged() error = %v, wantErr nil", err) + } +} + +func TestDiscardHandler_OnIndexPushed(t *testing.T) { + testDiscard := NewDiscardHandler() + if err := testDiscard.OnIndexPushed("test"); err != nil { + t.Errorf("DiscardHandler.OnIndexPushed() error = %v, wantErr nil", err) + } +} diff --git a/cmd/oras/internal/display/status/interface.go b/cmd/oras/internal/display/status/interface.go index c2f0bd8b8..0a918e42f 100644 --- a/cmd/oras/internal/display/status/interface.go +++ b/cmd/oras/internal/display/status/interface.go @@ -17,6 +17,8 @@ package status import ( "context" + + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" ) @@ -61,3 +63,19 @@ type CopyHandler interface { PostCopy(ctx context.Context, desc ocispec.Descriptor) error OnMounted(ctx context.Context, desc ocispec.Descriptor) error } + +// ManifestIndexCreateHandler handles status output for manifest index create command. +type ManifestIndexCreateHandler interface { + OnFetching(manifestRef string) error + OnFetched(manifestRef string, desc ocispec.Descriptor) error + OnIndexPacked(desc ocispec.Descriptor) error + OnIndexPushed(path string) error +} + +// ManifestIndexUpdateHandler handles status output for manifest index update command. +type ManifestIndexUpdateHandler interface { + ManifestIndexCreateHandler + OnManifestRemoved(digest digest.Digest) error + OnManifestAdded(manifestRef string, desc ocispec.Descriptor) error + OnIndexMerged(indexRef string, desc ocispec.Descriptor) error +} diff --git a/cmd/oras/internal/display/status/text.go b/cmd/oras/internal/display/status/text.go index c4277199f..76b54f09d 100644 --- a/cmd/oras/internal/display/status/text.go +++ b/cmd/oras/internal/display/status/text.go @@ -19,8 +19,11 @@ import ( "context" "sync" + "oras.land/oras/internal/contentutil" + "oras.land/oras/internal/descriptor" "oras.land/oras/internal/graph" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" @@ -179,3 +182,93 @@ func (ch *TextCopyHandler) OnMounted(_ context.Context, desc ocispec.Descriptor) ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ch.printer.PrintStatus(desc, copyPromptMounted) } + +// TextManifestIndexCreateHandler handles text status output for manifest index create events. +type TextManifestIndexCreateHandler struct { + printer *output.Printer +} + +// NewTextManifestIndexCreateHandler returns a new handler for manifest index create command. +func NewTextManifestIndexCreateHandler(printer *output.Printer) ManifestIndexCreateHandler { + tmich := TextManifestIndexCreateHandler{ + printer: printer, + } + return &tmich +} + +// OnFetching implements ManifestIndexCreateHandler. +func (mich *TextManifestIndexCreateHandler) OnFetching(source string) error { + return mich.printer.Println(IndexPromptFetching, source) +} + +// OnFetched implements ManifestIndexCreateHandler. +func (mich *TextManifestIndexCreateHandler) OnFetched(source string, _ ocispec.Descriptor) error { + return mich.printer.Println(IndexPromptFetched, source) +} + +// OnIndexPacked implements ManifestIndexCreateHandler. +func (mich *TextManifestIndexCreateHandler) OnIndexPacked(desc ocispec.Descriptor) error { + return mich.printer.Println(IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex) +} + +// OnIndexPushed implements ManifestIndexCreateHandler. +func (mich *TextManifestIndexCreateHandler) OnIndexPushed(path string) error { + return mich.printer.Println(IndexPromptPushed, path) +} + +// TextManifestIndexUpdateHandler handles text status output for manifest index update events. +type TextManifestIndexUpdateHandler struct { + printer *output.Printer +} + +// NewTextManifestIndexUpdateHandler returns a new handler for manifest index create command. +func NewTextManifestIndexUpdateHandler(printer *output.Printer) ManifestIndexUpdateHandler { + miuh := TextManifestIndexUpdateHandler{ + printer: printer, + } + return &miuh +} + +// OnFetching implements ManifestIndexUpdateHandler. +func (miuh *TextManifestIndexUpdateHandler) OnFetching(ref string) error { + return miuh.printer.Println(IndexPromptFetching, ref) +} + +// OnFetched implements ManifestIndexUpdateHandler. +func (miuh *TextManifestIndexUpdateHandler) OnFetched(ref string, desc ocispec.Descriptor) error { + if contentutil.IsDigest(ref) { + return miuh.printer.Println(IndexPromptFetched, ref) + } + return miuh.printer.Println(IndexPromptFetched, desc.Digest, ref) +} + +// OnManifestRemoved implements ManifestIndexUpdateHandler. +func (miuh *TextManifestIndexUpdateHandler) OnManifestRemoved(digest digest.Digest) error { + return miuh.printer.Println(IndexPromptRemoved, digest) +} + +// OnManifestAdded implements ManifestIndexUpdateHandler. +func (miuh *TextManifestIndexUpdateHandler) OnManifestAdded(ref string, desc ocispec.Descriptor) error { + if contentutil.IsDigest(ref) { + return miuh.printer.Println(IndexPromptAdded, ref) + } + return miuh.printer.Println(IndexPromptAdded, desc.Digest, ref) +} + +// OnIndexMerged implements ManifestIndexUpdateHandler. +func (miuh *TextManifestIndexUpdateHandler) OnIndexMerged(ref string, desc ocispec.Descriptor) error { + if contentutil.IsDigest(ref) { + return miuh.printer.Println(IndexPromptMerged, ref) + } + return miuh.printer.Println(IndexPromptMerged, desc.Digest, ref) +} + +// OnIndexPacked implements ManifestIndexUpdateHandler. +func (miuh *TextManifestIndexUpdateHandler) OnIndexPacked(desc ocispec.Descriptor) error { + return miuh.printer.Println(IndexPromptUpdated, desc.Digest) +} + +// OnIndexPushed implements ManifestIndexUpdateHandler. +func (miuh *TextManifestIndexUpdateHandler) OnIndexPushed(indexRef string) error { + return miuh.printer.Println(IndexPromptPushed, indexRef) +} diff --git a/cmd/oras/internal/display/status/text_test.go b/cmd/oras/internal/display/status/text_test.go index 8e68aa76b..223bc69da 100644 --- a/cmd/oras/internal/display/status/text_test.go +++ b/cmd/oras/internal/display/status/text_test.go @@ -193,3 +193,73 @@ func TestTextPushHandler_PreCopy(t *testing.T) { } validatePrinted(t, "Uploading 0b442c23c1dd oci-image") } + +func TestTextManifestIndexUpdateHandler_OnManifestAdded(t *testing.T) { + tests := []struct { + name string + printer *output.Printer + ref string + desc ocispec.Descriptor + wantErr bool + }{ + { + name: "ref is a digest", + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + ref: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", + desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, + wantErr: false, + }, + { + name: "ref is not a digest", + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + ref: "v1", + desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + miuh := &TextManifestIndexUpdateHandler{ + printer: tt.printer, + } + if err := miuh.OnManifestAdded(tt.ref, tt.desc); (err != nil) != tt.wantErr { + t.Errorf("TextManifestIndexUpdateHandler.OnManifestAdded() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestTextManifestIndexUpdateHandler_OnIndexMerged(t *testing.T) { + tests := []struct { + name string + printer *output.Printer + ref string + desc ocispec.Descriptor + wantErr bool + }{ + { + name: "ref is a digest", + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + ref: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", + desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, + wantErr: false, + }, + { + name: "ref is not a digest", + printer: output.NewPrinter(os.Stdout, os.Stderr, false), + ref: "v1", + desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + miuh := &TextManifestIndexUpdateHandler{ + printer: tt.printer, + } + if err := miuh.OnIndexMerged(tt.ref, tt.desc); (err != nil) != tt.wantErr { + t.Errorf("TextManifestIndexUpdateHandler.OnIndexMerged() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/cmd/oras/root/manifest/fetch.go b/cmd/oras/root/manifest/fetch.go index 74381cada..f7a8d7281 100644 --- a/cmd/oras/root/manifest/fetch.go +++ b/cmd/oras/root/manifest/fetch.go @@ -83,9 +83,6 @@ Example - Fetch raw manifest from an OCI layout archive file 'layout.tar': return fmt.Errorf("`--output -` cannot be used with `--format %s` at the same time", opts.Template) case opts.outputPath == "-" && opts.OutputDescriptor: return fmt.Errorf("`--descriptor` cannot be used with `--output -` at the same time") - // ignore --pretty when output to a file - case opts.outputPath != "" && opts.outputPath != "-": - opts.Pretty.Pretty = false } if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), "format", "pretty"); err != nil { return err diff --git a/cmd/oras/root/manifest/index/create.go b/cmd/oras/root/manifest/index/create.go index c8040559f..d44f8a32e 100644 --- a/cmd/oras/root/manifest/index/create.go +++ b/cmd/oras/root/manifest/index/create.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "fmt" - "os" "strings" "github.com/opencontainers/image-spec/specs-go" @@ -32,10 +31,10 @@ import ( "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/metadata" "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" @@ -109,7 +108,11 @@ func createIndex(cmd *cobra.Command, opts createOptions) error { if err != nil { return err } - manifests, err := fetchSourceManifests(ctx, target, opts) + displayStatus, displayMetadata, displayContent := display.NewManifestIndexCreateHandler(opts.outputPath, opts.Printer, opts.Pretty.Pretty) + if err != nil { + return err + } + manifests, err := fetchSourceManifests(ctx, displayStatus, target, opts.sources) if err != nil { return err } @@ -126,25 +129,26 @@ func createIndex(cmd *cobra.Command, opts createOptions) error { 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 + if err := displayStatus.OnIndexPacked(desc); err != nil { + return err + } + if err := displayContent.OnContentCreated(indexBytes); err != nil { + return err + } + if opts.outputPath == "" { + if err := pushIndex(ctx, displayStatus, displayMetadata, target, desc, indexBytes, opts.Reference, opts.extraRefs, opts.AnnotatedReference()); err != nil { + return err + } + } + return displayMetadata.OnCompleted(desc) } -func fetchSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts createOptions) ([]ocispec.Descriptor, error) { +func fetchSourceManifests(ctx context.Context, displayStatus status.ManifestIndexCreateHandler, target oras.ReadOnlyTarget, sources []string) ([]ocispec.Descriptor, error) { resolved := []ocispec.Descriptor{} - for _, source := range opts.sources { - opts.Println(status.IndexPromptFetching, source) + for _, source := range sources { + if err := displayStatus.OnFetching(source); err != nil { + return nil, err + } 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) @@ -152,7 +156,9 @@ func fetchSourceManifests(ctx context.Context, target oras.ReadOnlyTarget, opts if !descriptor.IsManifest(desc) { return nil, fmt.Errorf("%s is not a manifest", source) } - opts.Println(status.IndexPromptFetched, source) + if err := displayStatus.OnFetched(source, desc); err != nil { + return nil, err + } if desc, err = enrichDescriptor(ctx, target, desc, content); err != nil { return nil, err } @@ -184,7 +190,8 @@ func getPlatform(ctx context.Context, target oras.ReadOnlyTarget, manifestBytes 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 { +func pushIndex(ctx context.Context, displayStatus status.ManifestIndexCreateHandler, taggedHandler metadata.TaggedHandler, + target oras.Target, desc ocispec.Descriptor, content []byte, ref string, extraRefs []string, path string) error { // push the index var err error if ref == "" || contentutil.IsDigest(ref) { @@ -195,15 +202,16 @@ func pushIndex(ctx context.Context, target oras.Target, desc ocispec.Descriptor, if err != nil { return err } - printer.Println(status.IndexPromptPushed, path) + if err := displayStatus.OnIndexPushed(path); err != nil { + return err + } if len(extraRefs) != 0 { - handler := display.NewManifestIndexCreateHandler(printer) - tagListener := listener.NewTaggedListener(target, handler.OnTagged) + tagListener := listener.NewTaggedListener(target, taggedHandler.OnTagged) if _, err = oras.TagBytesN(ctx, tagListener, desc.MediaType, content, extraRefs, oras.DefaultTagBytesNOptions); err != nil { return err } } - return printer.Println("Digest:", desc.Digest) + return nil } func enrichDescriptor(ctx context.Context, target oras.ReadOnlyTarget, desc ocispec.Descriptor, manifestBytes []byte) (ocispec.Descriptor, error) { diff --git a/cmd/oras/root/manifest/index/create_test.go b/cmd/oras/root/manifest/index/create_test.go new file mode 100644 index 000000000..515302eb1 --- /dev/null +++ b/cmd/oras/root/manifest/index/create_test.go @@ -0,0 +1,141 @@ +/* +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" + "fmt" + "io" + "reflect" + "testing" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras/cmd/oras/internal/display/status" +) + +type testReadOnlyTarget struct { + content []byte +} + +func (tros *testReadOnlyTarget) Exists(ctx context.Context, desc ocispec.Descriptor) (bool, error) { + return true, nil +} + +func (tros *testReadOnlyTarget) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(tros.content)), nil +} + +func (tros *testReadOnlyTarget) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + if bytes.Equal(tros.content, []byte("index")) { + return ocispec.Descriptor{MediaType: ocispec.MediaTypeImageIndex, Digest: digest.FromBytes(tros.content), Size: int64(len(tros.content))}, nil + } + return ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest, Digest: digest.FromBytes(tros.content), Size: int64(len(tros.content))}, nil +} + +func NewTestReadOnlyTarget(text string) oras.ReadOnlyTarget { + return &testReadOnlyTarget{content: []byte(text)} +} + +type testCreateDisplayStatus struct { + onFetchingError bool + onFetchedError bool + onIndexPackedError bool + onIndexPushedError bool +} + +func (tds *testCreateDisplayStatus) OnFetching(manifestRef string) error { + if tds.onFetchingError { + return fmt.Errorf("OnFetching error") + } + return nil +} + +func (tds *testCreateDisplayStatus) OnFetched(manifestRef string, desc ocispec.Descriptor) error { + if tds.onFetchedError { + return fmt.Errorf("OnFetched error") + } + return nil +} + +func (tds *testCreateDisplayStatus) OnIndexPacked(desc ocispec.Descriptor) error { + if tds.onIndexPackedError { + return fmt.Errorf("error") + } + return nil +} + +func (tds *testCreateDisplayStatus) OnIndexPushed(path string) error { + if tds.onIndexPushedError { + return fmt.Errorf("error") + } + return nil +} + +func Test_fetchSourceManifests(t *testing.T) { + testContext := context.Background() + tests := []struct { + name string + ctx context.Context + displayStatus status.ManifestIndexCreateHandler + target oras.ReadOnlyTarget + sources []string + want []ocispec.Descriptor + wantErr bool + }{ + { + name: "OnFetching error", + ctx: testContext, + displayStatus: &testCreateDisplayStatus{onFetchingError: true}, + target: NewTestReadOnlyTarget("test content"), + sources: []string{"test"}, + want: nil, + wantErr: true, + }, + { + name: "OnFetched error", + ctx: testContext, + displayStatus: &testCreateDisplayStatus{onFetchedError: true}, + target: NewTestReadOnlyTarget("test content"), + sources: []string{"test"}, + want: nil, + wantErr: true, + }, + { + name: "getPlatform error", + ctx: testContext, + displayStatus: &testCreateDisplayStatus{}, + target: NewTestReadOnlyTarget("test content"), + sources: []string{"test"}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := fetchSourceManifests(tt.ctx, tt.displayStatus, tt.target, tt.sources) + if (err != nil) != tt.wantErr { + t.Errorf("fetchSourceManifests() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("fetchSourceManifests() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/oras/root/manifest/index/update.go b/cmd/oras/root/manifest/index/update.go index 7bff8a259..e82204519 100644 --- a/cmd/oras/root/manifest/index/update.go +++ b/cmd/oras/root/manifest/index/update.go @@ -19,7 +19,6 @@ import ( "context" "encoding/json" "fmt" - "os" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -29,10 +28,10 @@ import ( "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" "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" ) @@ -114,55 +113,58 @@ func updateIndex(cmd *cobra.Command, opts updateOptions) error { if err := opts.EnsureReferenceNotEmpty(cmd, true); err != nil { return err } - index, err := fetchIndex(ctx, target, opts) + displayStatus, displayMetadata, displayContent := display.NewManifestIndexUpdateHandler(opts.outputPath, opts.Printer, opts.Pretty.Pretty) + index, err := fetchIndex(ctx, displayStatus, target, opts.Reference) if err != nil { return err } - manifests, err := removeManifests(ctx, index.Manifests, target, opts) + manifests, err := removeManifests(displayStatus, index.Manifests, target, opts) if err != nil { return err } - manifests, err = addManifests(ctx, manifests, target, opts) + manifests, err = addManifests(ctx, displayStatus, manifests, target, opts.addArguments) if err != nil { return err } - manifests, err = mergeIndexes(ctx, manifests, target, opts) + manifests, err = mergeIndexes(ctx, displayStatus, manifests, target, opts.mergeArguments) 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) + if err := displayStatus.OnIndexPacked(desc); err != nil { + return err + } 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) + if err := displayContent.OnContentCreated(indexBytes); err != nil { + return err + } + if opts.outputPath == "" { + if err := pushIndex(ctx, displayStatus, displayMetadata, target, desc, indexBytes, opts.Reference, opts.tags, path); err != nil { + return err + } } - return err + return displayMetadata.OnCompleted(desc) } -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) +func fetchIndex(ctx context.Context, handler status.ManifestIndexUpdateHandler, target oras.ReadOnlyTarget, reference string) (ocispec.Index, error) { + if err := handler.OnFetching(reference); err != nil { + return ocispec.Index{}, err + } + desc, content, err := oras.FetchBytes(ctx, target, reference, oras.DefaultFetchBytesOptions) if err != nil { - return ocispec.Index{}, fmt.Errorf("could not find the index %s: %w", opts.Reference, err) + return ocispec.Index{}, fmt.Errorf("could not find the index %s: %w", reference, err) } if !descriptor.IsIndex(desc) { - return ocispec.Index{}, fmt.Errorf("%s is not an index", opts.Reference) + return ocispec.Index{}, fmt.Errorf("%s is not an index", reference) + } + if err := handler.OnFetched(reference, desc); err != nil { + return ocispec.Index{}, err } - 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 @@ -170,9 +172,11 @@ func fetchIndex(ctx context.Context, target oras.ReadOnlyTarget, opts updateOpti 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) +func addManifests(ctx context.Context, displayStatus status.ManifestIndexUpdateHandler, manifests []ocispec.Descriptor, target oras.ReadOnlyTarget, addArguments []string) ([]ocispec.Descriptor, error) { + for _, manifestRef := range addArguments { + if err := displayStatus.OnFetching(manifestRef); err != nil { + return nil, err + } 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) @@ -180,19 +184,25 @@ func addManifests(ctx context.Context, manifests []ocispec.Descriptor, target or if !descriptor.IsManifest(desc) { return nil, fmt.Errorf("%s is not a manifest", manifestRef) } - printUpdateStatus(status.IndexPromptFetched, manifestRef, string(desc.Digest), opts.Printer) + if err := displayStatus.OnFetched(manifestRef, desc); err != nil { + return nil, err + } if desc, err = enrichDescriptor(ctx, target, desc, content); err != nil { return nil, err } manifests = append(manifests, desc) - printUpdateStatus(status.IndexPromptAdded, manifestRef, string(desc.Digest), opts.Printer) + if err := displayStatus.OnManifestAdded(manifestRef, desc); err != nil { + return nil, err + } } 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) +func mergeIndexes(ctx context.Context, displayStatus status.ManifestIndexUpdateHandler, manifests []ocispec.Descriptor, target oras.ReadOnlyTarget, mergeArguments []string) ([]ocispec.Descriptor, error) { + for _, indexRef := range mergeArguments { + if err := displayStatus.OnFetching(indexRef); err != nil { + return nil, err + } 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) @@ -200,27 +210,31 @@ func mergeIndexes(ctx context.Context, manifests []ocispec.Descriptor, target or if !descriptor.IsIndex(desc) { return nil, fmt.Errorf("%s is not an index", indexRef) } - printUpdateStatus(status.IndexPromptFetched, indexRef, string(desc.Digest), opts.Printer) + if err := displayStatus.OnFetched(indexRef, desc); err != nil { + return nil, err + } 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) + if err := displayStatus.OnIndexMerged(indexRef, desc); err != nil { + return nil, err + } } return manifests, nil } -func removeManifests(ctx context.Context, manifests []ocispec.Descriptor, target oras.ReadOnlyTarget, opts updateOptions) ([]ocispec.Descriptor, error) { +func removeManifests(handler status.ManifestIndexUpdateHandler, 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) + return doRemoveManifests(manifests, digestToRemove, handler, opts.Reference) } -func doRemoveManifests(originalManifests []ocispec.Descriptor, digestToRemove map[digest.Digest]bool, printer *output.Printer, indexRef string) ([]ocispec.Descriptor, error) { +func doRemoveManifests(originalManifests []ocispec.Descriptor, digestToRemove map[digest.Digest]bool, handler status.ManifestIndexUpdateHandler, indexRef string) ([]ocispec.Descriptor, error) { manifests := []ocispec.Descriptor{} for _, m := range originalManifests { if _, exists := digestToRemove[m.Digest]; exists { @@ -233,7 +247,9 @@ func doRemoveManifests(originalManifests []ocispec.Descriptor, digestToRemove ma if !removed { return nil, fmt.Errorf("%s does not exist in the index %s", digest, indexRef) } - printUpdateStatus(status.IndexPromptRemoved, string(digest), "", printer) + if err := handler.OnManifestRemoved(digest); err != nil { + return nil, err + } } return manifests, nil } @@ -242,14 +258,6 @@ 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) diff --git a/cmd/oras/root/manifest/index/update_test.go b/cmd/oras/root/manifest/index/update_test.go index ed919727e..f3374bc61 100644 --- a/cmd/oras/root/manifest/index/update_test.go +++ b/cmd/oras/root/manifest/index/update_test.go @@ -16,15 +16,76 @@ limitations under the License. package index import ( - "os" + "context" + "fmt" "reflect" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "oras.land/oras/cmd/oras/internal/output" + "oras.land/oras-go/v2" + "oras.land/oras/cmd/oras/internal/display/status" ) +type testUpdateDisplayStatus struct { + onFetchingError bool + onFetchedError bool + onIndexPackedError bool + onIndexPushedError bool + onManifestRemovedError bool + onManifestAddedError bool + onIndexMergedError bool +} + +func (tds *testUpdateDisplayStatus) OnFetching(manifestRef string) error { + if tds.onFetchingError { + return fmt.Errorf("OnFetching error") + } + return nil +} + +func (tds *testUpdateDisplayStatus) OnFetched(manifestRef string, desc ocispec.Descriptor) error { + if tds.onFetchedError { + return fmt.Errorf("OnFetched error") + } + return nil +} + +func (tds *testUpdateDisplayStatus) OnIndexPacked(desc ocispec.Descriptor) error { + if tds.onIndexPackedError { + return fmt.Errorf("error") + } + return nil +} + +func (tds *testUpdateDisplayStatus) OnIndexPushed(path string) error { + if tds.onIndexPushedError { + return fmt.Errorf("error") + } + return nil +} + +func (tds *testUpdateDisplayStatus) OnManifestRemoved(digest digest.Digest) error { + if tds.onManifestRemovedError { + return fmt.Errorf("OnManifestRemoved error") + } + return nil +} + +func (tds *testUpdateDisplayStatus) OnManifestAdded(manifestRef string, desc ocispec.Descriptor) error { + if tds.onManifestAddedError { + return fmt.Errorf("error") + } + return nil +} + +func (tds *testUpdateDisplayStatus) OnIndexMerged(indexRef string, desc ocispec.Descriptor) error { + if tds.onIndexMergedError { + return fmt.Errorf("error") + } + return nil +} + var ( A = ocispec.Descriptor{ MediaType: ocispec.MediaTypeImageManifest, @@ -45,72 +106,81 @@ var ( 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 string + manifests []ocispec.Descriptor + digestSet map[digest.Digest]bool + displayStatus status.ManifestIndexUpdateHandler + 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 one matched item", + manifests: []ocispec.Descriptor{A, B, C}, + digestSet: map[digest.Digest]bool{B.Digest: false}, + displayStatus: &testUpdateDisplayStatus{}, + 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 all matched items", + manifests: []ocispec.Descriptor{A, B, A, C, A, A, A}, + digestSet: map[digest.Digest]bool{A.Digest: false}, + displayStatus: &testUpdateDisplayStatus{}, + 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 correctly when there is only one item", + manifests: []ocispec.Descriptor{A}, + digestSet: map[digest.Digest]bool{A.Digest: false}, + displayStatus: &testUpdateDisplayStatus{}, + 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 distinct manifests", + manifests: []ocispec.Descriptor{A, B, C}, + digestSet: map[digest.Digest]bool{A.Digest: false, C.Digest: false}, + displayStatus: &testUpdateDisplayStatus{}, + 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: "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}, + displayStatus: &testUpdateDisplayStatus{}, + indexRef: "test05", + 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, + name: "return error when deleting a nonexistent item", + manifests: []ocispec.Descriptor{A, C}, + digestSet: map[digest.Digest]bool{B.Digest: false}, + displayStatus: &testUpdateDisplayStatus{}, + indexRef: "test06", + want: nil, + wantErr: true, + }, + { + name: "handler error", + manifests: []ocispec.Descriptor{A, B, C}, + digestSet: map[digest.Digest]bool{B.Digest: false}, + displayStatus: &testUpdateDisplayStatus{onManifestRemovedError: true}, + indexRef: "test07", + 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) + got, err := doRemoveManifests(tt.manifests, tt.digestSet, tt.displayStatus, tt.indexRef) if (err != nil) != tt.wantErr { t.Errorf("removeManifestsFromIndex() error = %v, wantErr %v", err, tt.wantErr) return @@ -121,3 +191,113 @@ func Test_doRemoveManifests(t *testing.T) { }) } } + +func Test_fetchIndex(t *testing.T) { + testContext := context.Background() + tests := []struct { + name string + ctx context.Context + handler status.ManifestIndexUpdateHandler + target oras.ReadOnlyTarget + reference string + want ocispec.Index + wantErr bool + }{ + { + name: "OnFetching error", + ctx: testContext, + handler: &testUpdateDisplayStatus{onFetchingError: true}, + target: NewTestReadOnlyTarget("index"), + reference: "test", + want: ocispec.Index{}, + wantErr: true, + }, + { + name: "OnFetched error", + ctx: testContext, + handler: &testUpdateDisplayStatus{onFetchedError: true}, + target: NewTestReadOnlyTarget("index"), + reference: "test", + want: ocispec.Index{}, + wantErr: true, + }, + { + name: "Unmarshall error", + ctx: testContext, + handler: &testUpdateDisplayStatus{}, + target: NewTestReadOnlyTarget("index"), + reference: "test", + want: ocispec.Index{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := fetchIndex(tt.ctx, tt.handler, tt.target, tt.reference) + if (err != nil) != tt.wantErr { + t.Errorf("fetchIndex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("fetchIndex() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_mergeIndexes(t *testing.T) { + testContext := context.Background() + tests := []struct { + name string + ctx context.Context + displayStatus status.ManifestIndexUpdateHandler + manifests []ocispec.Descriptor + target oras.ReadOnlyTarget + mergeArguments []string + want []ocispec.Descriptor + wantErr bool + }{ + { + name: "OnFetching error", + ctx: testContext, + displayStatus: &testUpdateDisplayStatus{onFetchingError: true}, + manifests: []ocispec.Descriptor{}, + target: NewTestReadOnlyTarget("index"), + mergeArguments: []string{"test"}, + want: nil, + wantErr: true, + }, + { + name: "OnFetched error", + ctx: testContext, + displayStatus: &testUpdateDisplayStatus{onFetchedError: true}, + manifests: []ocispec.Descriptor{}, + target: NewTestReadOnlyTarget("index"), + mergeArguments: []string{"test"}, + want: nil, + wantErr: true, + }, + { + name: "unmarshall error", + ctx: testContext, + displayStatus: &testUpdateDisplayStatus{}, + manifests: []ocispec.Descriptor{}, + target: NewTestReadOnlyTarget("index"), + mergeArguments: []string{"test"}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := mergeIndexes(tt.ctx, tt.displayStatus, tt.manifests, tt.target, tt.mergeArguments) + if (err != nil) != tt.wantErr { + t.Errorf("mergeIndexes() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("mergeIndexes() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/test/e2e/suite/command/manifest_index.go b/test/e2e/suite/command/manifest_index.go index 0bcbd8a79..3560f5952 100644 --- a/test/e2e/suite/command/manifest_index.go +++ b/test/e2e/suite/command/manifest_index.go @@ -162,7 +162,7 @@ var _ = Describe("1.1 registry users:", 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() + "--output", "-").MatchContent(multi_arch.OutputIndex).Exec() }) It("should fail if given a reference that does not exist in the repo", func() { @@ -270,7 +270,7 @@ var _ = Describe("1.1 registry users:", func() { 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() + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-").MatchContent(multi_arch.OutputIndex).Exec() }) It("should tell user nothing to update if no update flags are used", func() { @@ -451,7 +451,7 @@ var _ = Describe("OCI image layout users:", 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() + "--output", "-").MatchContent(multi_arch.OutputIndex).Exec() }) It("should fail if given a reference that does not exist in the repo", func() { @@ -536,7 +536,7 @@ var _ = Describe("OCI image layout users:", func() { 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() + "--add", string(multi_arch.LinuxAMD64.Digest), "--output", "-").MatchContent(multi_arch.OutputIndex).Exec() }) It("should tell user nothing to update if no update flags are used", func() {