Skip to content

Commit

Permalink
refactor: list output handler (#1176)
Browse files Browse the repository at this point in the history
Refactor:
- moved the output logic of `list` command to `ListHandler` to simplify
list command code logic
- added `streamPrinter` to print tree node in streaming fashion

Test:
- improved E2E

Resolve part of #1151

---------

Signed-off-by: Junjie Gao <[email protected]>
  • Loading branch information
JeyJeyGao authored Feb 26, 2025
1 parent ae0d432 commit ba4c018
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 43 deletions.
6 changes: 6 additions & 0 deletions cmd/notation/internal/display/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,9 @@ func NewVerifyHandler(printer *output.Printer) metadata.VerifyHandler {
func NewBlobVerifyHandler(printer *output.Printer) metadata.BlobVerifyHandler {
return text.NewBlobVerifyHandler(printer)
}

// NewListHandler creates a new metadata ListHandler for rendering signature
// metadata information in a tree format.
func NewListHandler(printer *output.Printer) metadata.ListHandler {
return tree.NewListHandler(printer)
}
13 changes: 13 additions & 0 deletions cmd/notation/internal/display/metadata/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,16 @@ type BlobVerifyHandler interface {
// outcomes must not be nil or empty.
OnVerifySucceeded(outcomes []*notation.VerificationOutcome, blobPath string)
}

// ListHandler is a handler for rendering metadata information of a list of
// signatures.
type ListHandler interface {
Renderer

// OnReferenceResolved sets the artifact reference and media type for the
// handler.
OnReferenceResolved(reference string)

// OnSignatureListed adds the signature digest to be rendered.
OnSignatureListed(signatureManifest ocispec.Descriptor) error
}
78 changes: 78 additions & 0 deletions cmd/notation/internal/display/metadata/tree/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright The Notary Project 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 tree

import (
notationregistry "github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation/cmd/notation/internal/display/output"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

// ListHandler is a handler for rendering a list of signature digests in
// streaming fashion. It implements the metadata.ListHandler interface.
type ListHandler struct {
printer *output.Printer

// sprinter is a stream printer to print the signature digest nodes in
// a streaming fashion
sprinter *streamPrinter

// headerNode contains the headers of the output
//
// example:
// localhost:5000/net-monitor@sha256:b94d27b9934d3e08a52e52d7da7dabfac4efe37a5380ee9088f7ace2efcde9
// └── application/vnd.cncf.notary.signature
headerNode *node

// headerPrinted is a flag to indicate if the header has been printed
headerPrinted bool
}

// NewListHandler creates a new ListHandler.
func NewListHandler(printer *output.Printer) *ListHandler {
return &ListHandler{
printer: printer,
sprinter: newStreamPrinter(subTreePrefixLast, printer),
}
}

// OnReferenceResolved sets the artifact reference and media type for the
// handler.
func (h *ListHandler) OnReferenceResolved(reference string) {
h.headerNode = newNode(reference)
h.headerNode.Add(notationregistry.ArtifactTypeNotation)
}

// OnSignatureListed adds the signature digest to be printed.
func (h *ListHandler) OnSignatureListed(signatureManifest ocispec.Descriptor) error {
// print the header
if !h.headerPrinted {
if err := h.headerNode.Print(h.printer); err != nil {
return err
}
h.headerPrinted = true
}
return h.sprinter.PrintNode(newNode(signatureManifest.Digest.String()))
}

// Render completes the rendering of the list of signature digests.
func (h *ListHandler) Render() error {
if err := h.sprinter.Flush(); err != nil {
return err
}
if !h.headerPrinted {
return h.printer.Printf("%s has no associated signatures\n", h.headerNode.Value)
}
return nil
}
56 changes: 56 additions & 0 deletions cmd/notation/internal/display/metadata/tree/printer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright The Notary Project 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 tree

import "io"

// streamPrinter prints the tree nodes in a streaming fashion.
type streamPrinter struct {
w io.Writer
prefix string
prevNode *node
}

// newStreamPrinter creates a new stream printer.
//
// prefix is the prefix string that will be inherited by the nodes that are
// printed.
func newStreamPrinter(prefix string, w io.Writer) *streamPrinter {
return &streamPrinter{
w: w,
prefix: prefix,
}
}

// PrintNode adds a new node to be ready to print.
func (p *streamPrinter) PrintNode(node *node) error {
if p.prevNode == nil {
p.prevNode = node
return nil
}
if err := print(p.w, p.prefix, treeItemPrefix, p.prefix+subTreePrefix, p.prevNode); err != nil {
return err
}
p.prevNode = node
return nil
}

// Flush prints the last node and completes the printing.
func (p *streamPrinter) Flush() error {
if p.prevNode != nil {
// print the last node
return print(p.w, p.prefix, treeItemPrefixLast, p.prefix+subTreePrefixLast, p.prevNode)
}
return nil
}
123 changes: 123 additions & 0 deletions cmd/notation/internal/display/metadata/tree/printer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright The Notary Project 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 tree

import (
"bytes"
"testing"
)

func TestStreamingPrinter(t *testing.T) {
t.Run("empty output", func(t *testing.T) {
expected := ""
buff := &bytes.Buffer{}
p := newStreamPrinter("", buff)
p.Flush()

if buff.String() != expected {
t.Fatalf("expected %s, got %s", expected, buff.String())
}
})

t.Run("one node", func(t *testing.T) {
expected := "└── a\n"
buff := &bytes.Buffer{}
p := newStreamPrinter("", buff)
p.PrintNode(newNode("a"))
p.Flush()

if buff.String() != expected {
t.Fatalf("expected %s, got %s", expected, buff.String())
}
})

t.Run("two nodes", func(t *testing.T) {
expected := `├── a
└── b
`
buff := &bytes.Buffer{}
p := newStreamPrinter("", buff)
p.PrintNode(newNode("a"))
p.PrintNode(newNode("b"))
p.Flush()

if buff.String() != expected {
t.Fatalf("expected %s, got %s", expected, buff.String())
}
})

t.Run("two node with complex structure", func(t *testing.T) {
expected := `├── a
│ ├── b
│ │ └── c
│ └── d
└── e
├── f
│ └── g
└── h
`
buff := &bytes.Buffer{}
p := newStreamPrinter("", buff)
// create the tree
a := newNode("a")
b := a.Add("b")
b.Add("c")
a.Add("d")
p.PrintNode(a)

e := newNode("e")
f := e.Add("f")
f.Add("g")
e.Add("h")
p.PrintNode(e)

p.Flush()

if buff.String() != expected {
t.Fatalf("expected %s, got %s", expected, buff.String())
}
})

t.Run("two node with prefix", func(t *testing.T) {
expected := ` │ ├── a
│ │ ├── b
│ │ │ └── c
│ │ └── d
│ └── e
│ ├── f
│ │ └── g
│ └── h
`
buff := &bytes.Buffer{}
p := newStreamPrinter(" │ ", buff)
// create the tree
a := newNode("a")
b := a.Add("b")
b.Add("c")
a.Add("d")
p.PrintNode(a)

e := newNode("e")
f := e.Add("f")
f.Add("g")
e.Add("h")
p.PrintNode(e)

p.Flush()

if buff.String() != expected {
t.Fatalf("expected %s, got %s", expected, buff.String())
}
})
}
51 changes: 13 additions & 38 deletions cmd/notation/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ import (
"fmt"

notationregistry "github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation/cmd/notation/internal/display"
cmderr "github.com/notaryproject/notation/cmd/notation/internal/errors"
"github.com/notaryproject/notation/cmd/notation/internal/experimental"
"github.com/notaryproject/notation/cmd/notation/internal/option"
"github.com/notaryproject/notation/internal/cmd"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
)

type listOpts struct {
cmd.LoggingFlagOpts
SecureFlagOpts
option.Common
reference string
ociLayout bool
inputType inputType
Expand Down Expand Up @@ -73,6 +75,7 @@ Example - [Experimental] List signatures of an OCI artifact identified by a tag
if opts.ociLayout {
opts.inputType = inputTypeOCILayout
}
opts.Common.Parse(cmd)
return experimental.CheckFlagsAndWarn(cmd, "oci-layout")
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -95,60 +98,32 @@ func runList(ctx context.Context, opts *listOpts) error {
ctx = opts.LoggingFlagOpts.InitializeLogger(ctx)

// initialize
displayHandler := display.NewListHandler(opts.Printer)
reference := opts.reference
// always use the Referrers API, if not supported, automatically fallback to
// the referrers tag schema
sigRepo, err := getRepository(ctx, opts.inputType, reference, &opts.SecureFlagOpts, false)
if err != nil {
return err
}
targetDesc, resolvedRef, err := resolveReferenceWithWarning(ctx, opts.inputType, reference, sigRepo, "list")
manifestDesc, resolvedRef, err := resolveReference(ctx, opts.inputType, reference, sigRepo, func(ref string, manifestDesc ocispec.Descriptor) {
opts.Printer.PrintErrorf("Warning: Always list the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref)
})
if err != nil {
return err
}
// print all signature manifest digests
return printSignatureManifestDigests(ctx, targetDesc, sigRepo, resolvedRef, opts.maxSignatures)
}

// printSignatureManifestDigests returns the signature manifest digests of
// the subject manifest.
func printSignatureManifestDigests(ctx context.Context, targetDesc ocispec.Descriptor, sigRepo notationregistry.Repository, ref string, maxSigs int) error {
titlePrinted := false
printTitle := func() {
if !titlePrinted {
fmt.Println(ref)
fmt.Printf("└── %s\n", notationregistry.ArtifactTypeNotation)
titlePrinted = true
}
}
displayHandler.OnReferenceResolved(resolvedRef)

var prevDigest digest.Digest
err := listSignatures(ctx, sigRepo, targetDesc, maxSigs, func(sigManifestDesc ocispec.Descriptor) error {
// print the previous signature digest
if prevDigest != "" {
printTitle()
fmt.Printf(" ├── %s\n", prevDigest)
}
prevDigest = sigManifestDesc.Digest
return nil
})
// print the last signature digest
if prevDigest != "" {
printTitle()
fmt.Printf(" └── %s\n", prevDigest)
}
if err != nil {
// list signatures
if err := listSignatures(ctx, sigRepo, manifestDesc, opts.maxSignatures, displayHandler.OnSignatureListed); err != nil {
var errExceedMaxSignatures cmderr.ErrorExceedMaxSignatures
if !errors.As(err, &errExceedMaxSignatures) {
return err
}
fmt.Println("Warning:", errExceedMaxSignatures)
opts.Printer.PrintErrorf("Warning: %v\n", err)
}

if !titlePrinted {
fmt.Printf("%s has no associated signature\n", ref)
}
return nil
return displayHandler.Render()
}

// listSignatures lists signatures associated with manifestDesc with number of
Expand Down
Loading

0 comments on commit ba4c018

Please sign in to comment.