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 a114a9fe5..ce6243d9d 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 791dcb620..8480c60a6 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" ) @@ -63,3 +65,19 @@ type CopyHandler interface { StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) StopTracking() } + +// 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 dd0498f68..ef31ea85e 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" @@ -188,3 +191,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/docs/proposals/diagnose-experience.md b/docs/proposals/diagnose-experience.md new file mode 100644 index 000000000..d88e2920b --- /dev/null +++ b/docs/proposals/diagnose-experience.md @@ -0,0 +1,287 @@ +# Improve ORAS diagnose experience + +> [!NOTE] +> The version of this specification is v1.3.0 Beta.1. It is subject to change until ORAS v1.3.0 is released. + +ORAS v1.2.0 offers two global options, `--verbose` and `--debug`, which enable users to generate verbose output and logs respectively. These features facilitate both users and developers in inspecting ORAS's performance, interactions with external services and internal systems, and in diagnosing issues by providing a clear picture of the tool's operations. + +Given the diverse roles and scenarios in which ORAS CLI is utilized, we have received feedback from users and developers to improve the diagnostic experience. Enhancing debug logs can significantly benefit ORAS users and developers by making diagnostics clearer and more unambiguous. + +This proposal document aims to: + +1. Identify the issues associated with the current implementation of the `--verbose` and `--debug` options. +2. Clarify the concepts of different types of output and logs for diagnostic purposes. +3. List the guiding principles to write comprehensive, clear, and conducive debug output and debug logs for effective diagnosis. +4. Propose solutions to improve the diagnostic experience for ORAS CLI users and developers. + +## Problem Statement + +Specifically, there are existing GitHub issues raised in the ORAS community. + +- The user is confused about when to use `--verbose` and `--debug`. See the relevant issue [#1382](https://github.com/oras-project/oras/issues/1382). +- Poor readability of debug logs. No separator lines between request and response information. Users need to add separator lines manually for readability. See the relevant issue [#1382](https://github.com/oras-project/oras/issues/1382). +- Critical information is missing in debug logs. For example, the [error code](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes) and metadata of the processed resource object (e.g. image manifest) are not displayed. +- The detailed operation information is missing in verbose output. For example, how many and where are objects processed. Less or no verbose output of ORAS commands in some use cases. +- The user environment information is not printed out. This causes a higher cost to reproduce the issues for ORAS developers locally. +- Timestamp of each request and response is missing in debug logs, which is hard to trace historical operation and trace the sequence of events accurately. + +## Concepts + +At first, the output of ORAS flag `--verbose` and `--debug` should be clarified before restructuring them. + +### Output + +There are four types of output in ORAS CLI: + +- **Status output**: such as progress information, progress bar in pulling or pushing files. +- **Metadata output**: showing what has been pulled (e.g. filename, digest, etc.) in specified format, such as JSON, text. +- **Content output**: it is to output the raw data obtained from the remote registry server or file system, such as the pulled artifact content saved as a file. +- **Error output**: error message are expected to be helpful to troubleshoot where the user has done something wrong and the program is guiding them in the right direction. + +The target users of these types of output are general users. + +Currently, the output of ORAS `--verbose` flag only exists in oras `pull/push/attach/discover` commands, which prints out detailed status output and metadata output. Since ORAS v1.2.0, progress bar is enabled in `pull/push/attach` by default, thus the ORAS output is already verbose on a terminal. + +The original output of ORAS `--verbose` is intended for end-users who want to observe the detailed file operation when using ORAS. It gives users a comprehensive view of what the tool is doing at every step and how long does it take when push or pull a file. + +### Logs + +Logs focus on providing technical details for in-depth diagnosing and troubleshooting issues. It is intended for developers or technical users who need to understand the inner workings of the tool. Debug logs are detailed and technical, often including HTTP request and response from interactions between client and server, as well as code-specific information. In general, there are different levels of logs. [Logrus](https://github.com/sirupsen/logrus) has been used by ORAS, which has seven logging levels: `Trace`, `Debug`, `Info`, `Warning`, `Error`, `Fatal` and `Panic`, but only `DEBUG` level log is used in ORAS, which is controlled by the flag `--debug`. + +- **Purpose**: Debug logs are specifically aimed to facilitate ORAS developers to diagnose ORAS tool itself. They contain detailed technical information that is useful for troubleshooting problems. +- **Target users**: Primarily intended for developers or technical users who are trying to understand the inner workings of the code and identify the root cause of a possible issue with the tool itself. +- **Content**: Debug logs focus on providing context needed to troubleshoot issues, like variable values, execution paths, error stack traces, and internal states of the application. +- **Level of Detail**: Extremely detailed, providing insights into the application's internal workings and logic, often including low-level details that are essential for debugging. + +### Common Conventions + +Here are the common conventions to print clear and analyzable debug logs. + +### **Timestamp Each Log Entry** +- **Precise Timing:** Ensure each log entry has a precise timestamp to trace the sequence of events accurately. ORAS SHOULD use the [Nanoseconds](https://pkg.go.dev/time#Duration.Nanoseconds) precision to print the timestamp in the first field of each line. + - Example: `[2024-08-02 23:56:02.6738192Z] Starting metadata retrieval for repository oras-demo` + +### **Avoid Logging Sensitive Information** +- **Privacy and Security:** Abstain from logging sensitive information such as passwords, personal data, or authentication tokens. + - Example: `[2024-08-02 23:56:02.7338192Z] Attempting to authenticate user [UserID: usr123]` (exclude authentication token and password information). + +## Proposals for ORAS CLI + +Based on the concepts and conventions above, here are the proposal for ORAS diagnose experience improvement: + +- Deprecate the `--verbose` flag and keep `--debug` flag to avoid ambiguity. It is reasonable to continue using `--debug` to enable the output of `DEBUG` level logs as it is in ORAS. Meanwhile, this change will make the diagnose experience much more straightforward and less breaking since only ORAS `pull/push/attach/discover` commands have verbose output. +- Make the verbose output of commands `pull`, `push`, `attach` as the default (status) output. See examples at the bottom. +- Make the verbose output of command `discover` as a formatted output, controlled by `--format tree-full`. +- Add two empty lines as the separator between each request and response for readability. +- Add timestamp of each request and response to the beginning of each request and response. +- Print out the response body in the debug logs if the [Content-Type](https://www.rfc-editor.org/rfc/rfc2616#section-14.17) of the HTTP response body is JSON or plain text format. ORAS SHOULD limit the size of a response body for preventing clients from overloading the registry server with massive payloads that could lead to performance issues. +- Upon a response with failures, the [error code](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes) should be included in the response body by default for diagnose purposes. +- Considering ORAS is not a daemon service so parsing debug logs to a logging system is not a common scenario. The target users of the debug logs are normal users and ORAS developers. Thereby, the debug logs in TTY mode and non-TTY (`--no-tty`) should be consistent, except for the color. Specifically, debug logs SHOULD be colored-code in a TTY mode for better readability on terminal but keeping plain text in a non-TTY mode. +- Summarize common conventions for writing clear and analyzable debug logs. +- Show running environment details of ORAS such as `OS/Arch` in the output of `oras version`. It would be helpful to help the ORAS developers locate and reproduce the issue easier. + +## Investigation on other CLI tools + +To make sure the ORAS diagnose functions are natural and easier to use, it worth knowing how diagnose functions work in other popular client tools. + +#### Curl + +Curl only has a `--verbose` option to output verbose logs. No `--debug` option. + +#### Docker and Podman + +Docker provides two options `--debug` and `--log-level` to control debug logs output within different log levels, such as INFO, DEBUG, WARN, etc. No `--verbose` option. Docker has its own daemon service running in local so its logs might be much more complex. + +#### Helm + +Helm CLI tool provides a global flag `--debug` to enable verbose output. + +## Examples in ORAS + +This section lists the current behaviors of ORAS debug logs, proposes the suggested changes to ORAS CLI commands. + +### oras copy + +Take the first two requests and responses from its debug logs as examples: + +``` +oras copy ghcr.io/oras-project/oras:v1.2.0 --to-oci-layout oras-dev:v1.2.0 --debug +``` + +**Current debug log in TTY mode** + +``` +DEBU[0000] Request #0 +> Request URL: "https://ghcr.io/v2/oras-project/oras/manifests/v1.2.0" +> Request method: "GET" +> Request headers: + "User-Agent": "oras/1.2.0+Homebrew" + "Accept": "application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.oci.artifact.manifest.v1+json" +DEBU[0001] Response #0 +< Response Status: "401 Unauthorized" +< Response headers: + "Content-Length": "73" + "X-Github-Request-Id": "9FC6:30019C:17C06:1C462:66AD0463" + "Content-Type": "application/json" + "Www-Authenticate": "Bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:oras-project/oras:pull\"" + "Date": "Fri, 02 Aug 2024 16:08:04 GMT" +DEBU[0001] Request #1 +> Request URL: "https://ghcr.io/token?scope=repository%3Aoras-project%2Foras%3Apull&service=ghcr.io" +> Request method: "GET" +> Request headers: + "User-Agent": "oras/1.2.0+Homebrew" +DEBU[0002] Response #1 +< Response Status: "200 OK" +< Response headers: + "Content-Type": "application/json" + "Docker-Distribution-Api-Version": "registry/2.0" + "Date": "Fri, 02 Aug 2024 16:08:05 GMT" + "Content-Length": "69" + "X-Github-Request-Id": "9FC6:30019C:17C0D:1C46C:66AD0464" +``` + +**Current debug log in non-TTY mode** + +``` +time=2024-11-13T00:08:03-08:00 level=debug msg=Request #0 +> Request URL: "https://ghcr.io/v2/oras-project/oras/manifests/v1.2.0" +> Request method: "GET" +> Request headers: + "Accept": "application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.oci.artifact.manifest.v1+json" + "User-Agent": "oras/1.2.0+Homebrew" +time=2024-11-13T00:08:04-08:00 level=debug msg=Response #0 +< Response Status: "401 Unauthorized" +< Response headers: + "Content-Type": "application/json" + "Www-Authenticate": "Bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:oras-project/oras:pull\"" + "Date": "Wed, 13 Nov 2024 08:08:04 GMT" + "Content-Length": "73" + "X-Github-Request-Id": "A976:6E843:5D1712B:5F3E769:67345E64" +time=2024-11-13T00:08:04-08:00 level=debug msg=Request #1 +> Request URL: "https://ghcr.io/token?scope=repository%3Aoras-project%2Foras%3Apull&service=ghcr.io" +> Request method: "GET" +> Request headers: + "User-Agent": "oras/1.2.0+Homebrew" +time=2024-11-13T00:08:04-08:00 level=debug msg=Response #1 +< Response Status: "200 OK" +< Response headers: + "X-Github-Request-Id": "A976:6E843:5D171B4:5F3E7EE:67345E64" + "Content-Type": "application/json" + "Docker-Distribution-Api-Version": "registry/2.0" + "Date": "Wed, 13 Nov 2024 08:08:04 GMT" + "Content-Length": "69" +``` + +**Suggested debug logs in TTY and Non-TTY mode:** + +The debug logs in TTY mode and non-TTY (`--no-tty`) should be consistent, except for the color. + +``` +[2024-08-02 23:56:02.6738192Z] --> Request #0 +> Request URL: "https://ghcr.io/v2/oras-project/oras/manifests/v1.2.0" +> Request method: "GET" +> Request headers: + "User-Agent": "oras/1.2.0+Homebrew" + "Accept": "application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.oci.artifact.manifest.v1+json" + + +[2024-08-02 23:56:03.6738192Z] <-- Response #0 +< Response Status: "401 Unauthorized" +< Response headers: + "Content-Length": "73" + "X-Github-Request-Id": "9FC6:30019C:17C06:1C462:66AD0463" + "Content-Type": "application/json" + "Www-Authenticate": "Bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:oras-project/oras:pull\"" + "Date": "Fri, 02 Aug 2024 23:56:03 GMT" +< Response body: +{ + "errors": [ + { + "code": "", + "message": "", + "detail": "" + }, + ... + ] +} + + +[2024-08-02 23:56:04.6738192Z] --> Request #1 +> Request URL: "https://ghcr.io/token?scope=repository%3Aoras-project%2Foras%3Apull&service=ghcr.io" +> Request method: "GET" +> Request headers: + "User-Agent": "oras/1.2.0+Homebrew" + + +[2024-08-02 23:56:04.6738192Z] <-- Response #1 +< Response Status: "200 OK" +< Response headers: + "Content-Type": "application/json" + "Docker-Distribution-Api-Version": "registry/2.0" + "Date": "Fri, 02 Aug 2024 16:08:05 GMT" + "Content-Length": "69" + "X-Github-Request-Id": "9FC6:30019C:17C0D:1C46C:66AD0464" +< Response body: +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:42c524c48e0672568dbd2842d3a0cb34a415347145ee9fe1c8abaf65e7455b46", + "size": 1239, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + ยทยทยท +} + + +``` + +### oras push/pull/attach + +The verbose output of commands `pull`, `push`, `attach` is now printed out in the default (status) output, the `--verbose` is no longer needed. Considering the progress bar is shown on terminal by default, the verbose output should be available on non-terminal environment using `--no-tty`. See `oras pull` command as an example: + +```bash +$ oras push --oci-layout layout-test:sample README.md --no-tty + +Preparing README.md +Uploading 9500d720111f README.md +Uploading 44136fa355b3 application/vnd.oci.empty.v1+json +Uploaded 44136fa355b3 application/vnd.oci.empty.v1+json +Uploaded 9500d720111f README.md +Uploading 655f5cc5d5d2 application/vnd.oci.image.manifest.v1+json +Uploaded 655f5cc5d5d2 application/vnd.oci.image.manifest.v1+json +Pushed [oci-layout] layout-test:sample +ArtifactType: application/vnd.unknown.artifact.v1 +Digest: sha256:655f5cc5d5d2e7ff2ab90378514103996523b065ce5ef43cd6e6a4de7d80a535 +``` + +### Show user's environment details + +Output the user's running environment details of ORAS such as operating system and architecture information would be helpful to help the ORAS developers locate the issue and reproduce easier. + +For example, the operating system and architecture are supposed to be outputted in `oras version`: + +```bash +$ oras version + +ORAS Version: 1.2.0+Homebrew +Go version: go1.22.3 +OS/Arch: linux/amd64 +Git commit: xxxxxxxxxxxx +Git tree state: clean +``` + +## Q & A + +**Q1:** Is it a common practice to use an environment variable like export ORAS_DEBUG=1 as a global switch for debug logs? What are the Pros and Cons of using this design? + +**A:** Per our discussion in the ORAS community meeting, ORAS maintainers agreed to not introduce an additional environment variable as a global switch to enable debug logs since --debug is intuitive enough. + +**Q2:** For the diagnose flag options, why deprecate `--verbose` and remain `--debug` as it is? + +**A:** The major reason is that this change avoids overloading the flag `--verbose` and reduce ambiguity in ORAS diagnose experience. Moreover, the `--debug` is consistent with other popular container client tools, such as Helm and Docker. Deprecation of `--verbose` is less breaking than changing behaviors of `--debug`. \ No newline at end of file diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 8a23ac1d2..5ed527f05 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -3,7 +3,7 @@ module oras.land/oras/test/e2e go 1.23.0 require ( - github.com/onsi/ginkgo/v2 v2.21.0 + github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.35.1 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0 diff --git a/test/e2e/go.sum b/test/e2e/go.sum index c12d67b5d..45d83ea10 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -8,8 +8,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/test/e2e/scripts/prepare.sh b/test/e2e/scripts/prepare.sh index 13b119574..ffcee4f05 100755 --- a/test/e2e/scripts/prepare.sh +++ b/test/e2e/scripts/prepare.sh @@ -35,6 +35,7 @@ echo " === installing ginkgo === " repo_root=$(realpath --canonicalize-existing ${repo_root}) cwd=$(pwd) cd ${repo_root}/test/e2e && go install github.com/onsi/ginkgo/v2/ginkgo@latest +export PATH=$(go env GOPATH)/bin:$PATH trap "cd $cwd" EXIT # start registries 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() {