Skip to content

Commit bf8e2ee

Browse files
feat!: support showing indirect referrers for all formats of oras discover (#1653)
Signed-off-by: Xiaoxuan Wang <[email protected]>
1 parent 26551f7 commit bf8e2ee

File tree

10 files changed

+328
-165
lines changed

10 files changed

+328
-165
lines changed

cmd/oras/internal/display/metadata/interface.go

-3
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ type AttachHandler interface {
4444
type DiscoverHandler interface {
4545
Renderer
4646

47-
// MultiLevelSupported returns true if the handler supports multi-level
48-
// discovery.
49-
MultiLevelSupported() bool
5047
// OnDiscovered is called after a referrer is discovered.
5148
OnDiscovered(referrer, subject ocispec.Descriptor) error
5249
}

cmd/oras/internal/display/metadata/json/discover.go

+9-21
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,36 @@ limitations under the License.
1616
package json
1717

1818
import (
19-
"fmt"
2019
"io"
2120

2221
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
23-
"oras.land/oras-go/v2/content"
2422
"oras.land/oras/cmd/oras/internal/display/metadata"
2523
"oras.land/oras/cmd/oras/internal/display/metadata/model"
2624
"oras.land/oras/cmd/oras/internal/output"
2725
)
2826

2927
// discoverHandler handles json metadata output for discover events.
3028
type discoverHandler struct {
31-
out io.Writer
32-
root ocispec.Descriptor
33-
path string
34-
referrers []ocispec.Descriptor
29+
out io.Writer
30+
path string
31+
model model.Discover
3532
}
3633

3734
// NewDiscoverHandler creates a new handler for discover events.
38-
func NewDiscoverHandler(out io.Writer, root ocispec.Descriptor, path string) metadata.DiscoverHandler {
35+
func NewDiscoverHandler(out io.Writer, subject ocispec.Descriptor, path string) metadata.DiscoverHandler {
3936
return &discoverHandler{
40-
out: out,
41-
root: root,
42-
path: path,
37+
out: out,
38+
path: path,
39+
model: model.NewDiscover(path, subject),
4340
}
4441
}
4542

46-
// MultiLevelSupported implements metadata.DiscoverHandler.
47-
func (h *discoverHandler) MultiLevelSupported() bool {
48-
return false
49-
}
50-
5143
// OnDiscovered implements metadata.DiscoverHandler.
5244
func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error {
53-
if !content.Equal(subject, h.root) {
54-
return fmt.Errorf("unexpected subject descriptor: %v", subject)
55-
}
56-
h.referrers = append(h.referrers, referrer)
57-
return nil
45+
return h.model.AddReferrer(referrer, subject)
5846
}
5947

6048
// Render implements metadata.DiscoverHandler.
6149
func (h *discoverHandler) Render() error {
62-
return output.PrintPrettyJSON(h.out, model.NewDiscover(h.path, h.root, h.referrers))
50+
return output.PrintPrettyJSON(h.out, h.model.Root)
6351
}

cmd/oras/internal/display/metadata/model/discover.go

+42-10
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,53 @@ limitations under the License.
1515

1616
package model
1717

18-
import ocispec "github.com/opencontainers/image-spec/specs-go/v1"
18+
import (
19+
"fmt"
1920

20-
type discover struct {
21+
"github.com/opencontainers/go-digest"
22+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
23+
)
24+
25+
// Discover is a model for discovered referrers.
26+
type Discover struct {
27+
name string
28+
nodes map[digest.Digest]*Node
29+
Root *Node
30+
}
31+
32+
// Node represents a node in the discovered reference tree.
33+
type Node struct {
2134
Descriptor
22-
Referrers []Descriptor `json:"referrers"`
35+
Referrers []*Node `json:"referrers,omitempty"`
36+
}
37+
38+
// AddReferrer adds a node to the discovered referrers tree.
39+
func (d *Discover) AddReferrer(referrer, subject ocispec.Descriptor) error {
40+
to, ok := d.nodes[subject.Digest]
41+
if !ok {
42+
return fmt.Errorf("unexpected subject descriptor: %v", subject)
43+
}
44+
from := NewNode(d.name, referrer)
45+
d.nodes[from.Digest] = from
46+
to.Referrers = append(to.Referrers, from)
47+
return nil
2348
}
2449

2550
// NewDiscover creates a new discover model.
26-
func NewDiscover(name string, subject ocispec.Descriptor, referrers []ocispec.Descriptor) discover {
27-
discover := discover{
28-
Descriptor: FromDescriptor(name, subject),
29-
Referrers: make([]Descriptor, 0, len(referrers)),
51+
func NewDiscover(path string, root ocispec.Descriptor) Discover {
52+
treeRoot := NewNode(path, root)
53+
return Discover{
54+
name: path,
55+
nodes: map[digest.Digest]*Node{
56+
root.Digest: treeRoot,
57+
},
58+
Root: treeRoot,
3059
}
31-
for _, referrer := range referrers {
32-
discover.Referrers = append(discover.Referrers, FromDescriptor(name, referrer))
60+
}
61+
62+
// NewNode creates a new node.
63+
func NewNode(name string, desc ocispec.Descriptor) *Node {
64+
return &Node{
65+
Descriptor: FromDescriptor(name, desc),
3366
}
34-
return discover
3567
}

cmd/oras/internal/display/metadata/table/discover.go

-5
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ func NewDiscoverHandler(out io.Writer, rawReference string, root ocispec.Descrip
4545
}
4646
}
4747

48-
// MultiLevelSupported implements metadata.DiscoverHandler.
49-
func (h *discoverHandler) MultiLevelSupported() bool {
50-
return false
51-
}
52-
5348
// OnDiscovered implements metadata.DiscoverHandler.
5449
func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error {
5550
if !content.Equal(subject, h.root) {

cmd/oras/internal/display/metadata/template/discover.go

+7-19
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,38 @@ limitations under the License.
1616
package template
1717

1818
import (
19-
"fmt"
2019
"io"
2120

2221
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
23-
"oras.land/oras-go/v2/content"
2422
"oras.land/oras/cmd/oras/internal/display/metadata"
2523
"oras.land/oras/cmd/oras/internal/display/metadata/model"
2624
"oras.land/oras/cmd/oras/internal/output"
2725
)
2826

2927
// discoverHandler handles json metadata output for discover events.
3028
type discoverHandler struct {
31-
referrers []ocispec.Descriptor
32-
template string
33-
path string
34-
root ocispec.Descriptor
35-
out io.Writer
29+
template string
30+
path string
31+
out io.Writer
32+
model model.Discover
3633
}
3734

3835
// NewDiscoverHandler creates a new handler for discover events.
3936
func NewDiscoverHandler(out io.Writer, root ocispec.Descriptor, path string, template string) metadata.DiscoverHandler {
4037
return &discoverHandler{
4138
out: out,
42-
root: root,
4339
path: path,
4440
template: template,
41+
model: model.NewDiscover(path, root),
4542
}
4643
}
4744

48-
// MultiLevelSupported implements metadata.DiscoverHandler.
49-
func (h *discoverHandler) MultiLevelSupported() bool {
50-
return false
51-
}
52-
5345
// OnDiscovered implements metadata.DiscoverHandler.
5446
func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error {
55-
if !content.Equal(subject, h.root) {
56-
return fmt.Errorf("unexpected subject descriptor: %v", subject)
57-
}
58-
h.referrers = append(h.referrers, referrer)
59-
return nil
47+
return h.model.AddReferrer(referrer, subject)
6048
}
6149

6250
// Render implements metadata.DiscoverHandler.
6351
func (h *discoverHandler) Render() error {
64-
return output.ParseAndWrite(h.out, model.NewDiscover(h.path, h.root, h.referrers), h.template)
52+
return output.ParseAndWrite(h.out, h.model.Root, h.template)
6553
}

cmd/oras/internal/display/metadata/tree/discover.go

-5
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ func NewDiscoverHandler(out io.Writer, path string, root ocispec.Descriptor, ver
5050
}
5151
}
5252

53-
// MultiLevelSupported implements metadata.DiscoverHandler.
54-
func (h *discoverHandler) MultiLevelSupported() bool {
55-
return true
56-
}
57-
5853
// OnDiscovered implements metadata.DiscoverHandler.
5954
func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error {
6055
node, ok := h.nodes[subject.Digest]

cmd/oras/internal/option/format.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ var (
5959
// the table format is deprecated
6060
FormatTypeTable = &FormatType{
6161
Name: "table",
62-
Usage: "[Deprecated] Get direct referrers and output in table format",
62+
Usage: "[Deprecated] Get referrers and output in table format",
6363
}
6464
FormatTypeTree = &FormatType{
6565
Name: "tree",
66-
Usage: "Get referrers recursively and print in tree format",
66+
Usage: "Get referrers and print in tree format",
6767
}
6868
FormatTypeText = &FormatType{
6969
Name: "text",

cmd/oras/root/discover.go

+27-20
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type discoverOptions struct {
4141

4242
artifactType string
4343
verbose bool
44+
depth int
4445
}
4546

4647
func discoverCmd() *cobra.Command {
@@ -67,6 +68,9 @@ Example - [Experimental] Discover referrers and display in a table view:
6768
Example - [Experimental] Discover referrers and format output with Go template:
6869
oras discover localhost:5000/hello:v1 --format go-template --template "{{.referrers}}"
6970
71+
Example - [Experimental] Discover only direct referrers, displayed in json view:
72+
oras discover localhost:5000/hello:v1 --format json --depth 1
73+
7074
Example - Discover all the referrers of manifest with annotations, displayed in a tree view:
7175
oras discover -v localhost:5000/hello:v1
7276
@@ -81,13 +85,20 @@ Example - Discover referrers of the manifest tagged 'v1' in an OCI image layout
8185
if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), "format", "output"); err != nil {
8286
return err
8387
}
88+
if cmd.Flags().Changed("depth") && opts.depth < 1 {
89+
return errors.New("depth value should be at least 1")
90+
}
91+
// only show direct referrers for table format
92+
if opts.FormatFlag == option.FormatTypeTable.Name {
93+
opts.depth = 1
94+
}
8495
opts.RawReference = args[0]
8596
if err := option.Parse(cmd, &opts); err != nil {
8697
return err
8798
}
8899
if cmd.Flags().Changed("output") {
89100
switch opts.Format.Type {
90-
case "tree", "json", "table":
101+
case option.FormatTypeTree.Name, option.FormatTypeJSON.Name, option.FormatTypeTable.Name:
91102
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "[DEPRECATED] --output is deprecated, try `--format %s` instead\n", opts.Template)
92103
default:
93104
return errors.New("output type can only be tree, table or json")
@@ -101,13 +112,14 @@ Example - Discover referrers of the manifest tagged 'v1' in an OCI image layout
101112
}
102113

103114
cmd.Flags().StringVarP(&opts.artifactType, "artifact-type", "", "", "artifact type")
104-
cmd.Flags().StringVarP(&opts.Format.FormatFlag, "output", "o", "tree", "[Deprecated] format in which to display referrers (table, json, or tree). tree format will also show indirect referrers")
115+
cmd.Flags().StringVarP(&opts.Format.FormatFlag, "output", "o", "tree", "[Deprecated] format in which to display referrers (table, json, or tree).")
105116
cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "display full metadata of referrers")
117+
cmd.Flags().IntVarP(&opts.depth, "depth", "", 0, "[Experimental] level of referrers to display, if unused shows referrers of all levels")
106118
opts.SetTypes(
107119
option.FormatTypeTree,
108120
option.FormatTypeTable,
109-
option.FormatTypeJSON.WithUsage("Get direct referrers and output in JSON format"),
110-
option.FormatTypeGoTemplate.WithUsage("Print direct referrers using the given Go template"),
121+
option.FormatTypeJSON.WithUsage("Get referrers and output in JSON format"),
122+
option.FormatTypeGoTemplate.WithUsage("Print referrers using the given Go template"),
111123
)
112124
opts.EnableDistributionSpecFlag()
113125
option.ApplyFlags(&opts, cmd.Flags())
@@ -136,39 +148,34 @@ func runDiscover(cmd *cobra.Command, opts *discoverOptions) error {
136148
if err != nil {
137149
return err
138150
}
139-
if handler.MultiLevelSupported() {
140-
if err := fetchAllReferrers(ctx, repo, desc, opts.artifactType, handler); err != nil {
141-
return err
142-
}
143-
} else {
144-
refs, err := registry.Referrers(ctx, repo, desc, opts.artifactType)
145-
if err != nil {
146-
return err
147-
}
148-
for _, ref := range refs {
149-
if err := handler.OnDiscovered(ref, desc); err != nil {
150-
return err
151-
}
152-
}
151+
if err := fetchAllReferrers(ctx, repo, desc, opts.artifactType, handler, opts.depth); err != nil {
152+
return err
153153
}
154154
return handler.Render()
155155
}
156156

157-
func fetchAllReferrers(ctx context.Context, repo oras.ReadOnlyGraphTarget, desc ocispec.Descriptor, artifactType string, handler metadata.DiscoverHandler) error {
157+
func fetchAllReferrers(ctx context.Context, repo oras.ReadOnlyGraphTarget, desc ocispec.Descriptor, artifactType string, handler metadata.DiscoverHandler, depth int) error {
158158
results, err := registry.Referrers(ctx, repo, desc, artifactType)
159159
if err != nil {
160160
return err
161161
}
162162

163+
var nextDepth int
164+
if depth > 0 {
165+
nextDepth = depth - 1
166+
}
163167
for _, r := range results {
164168
if err := handler.OnDiscovered(r, desc); err != nil {
165169
return err
166170
}
171+
if depth == 1 {
172+
continue
173+
}
167174
if err := fetchAllReferrers(ctx, repo, ocispec.Descriptor{
168175
Digest: r.Digest,
169176
Size: r.Size,
170177
MediaType: r.MediaType,
171-
}, artifactType, handler); err != nil {
178+
}, artifactType, handler, nextDepth); err != nil {
172179
return err
173180
}
174181
}

test/e2e/internal/testdata/foobar/const.go

+4
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ var (
130130
MediaType: "application/vnd.oci.image.manifest.v1+json",
131131
Digest: digest.Digest("sha256:0cb8c4da7e9ff2e7eefca33141091b9239218e3125a35e17e8bcd05fa3a5e714"),
132132
Size: 670,
133+
Annotations: map[string]string{
134+
"org.opencontainers.image.created": "2023-01-18T08:37:57Z",
135+
},
136+
ArtifactType: "test/signature.file",
133137
}
134138
SBOMArtifactReferrer = ocispec.Descriptor{
135139
MediaType: "application/vnd.oci.artifact.manifest.v1+json",

0 commit comments

Comments
 (0)