mirror of
https://github.com/regclient/regclient.git
synced 2025-04-18 22:44:00 +03:00
Feat: Include source in referrers response
This simplifies handling external sources. Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
parent
d73d40b126
commit
763599514d
@ -1082,7 +1082,7 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
|
||||
}
|
||||
|
||||
seen := []string{}
|
||||
tr, err := artifactOpts.treeAddResult(ctx, rc, r, rRefSrc, seen, referrerOpts, tags)
|
||||
tr, err := artifactOpts.treeAddResult(ctx, rc, r, seen, referrerOpts, tags)
|
||||
var twErr error
|
||||
if tr != nil {
|
||||
twErr = template.Writer(cmd.OutOrStdout(), artifactOpts.formatTree, tr)
|
||||
@ -1093,7 +1093,7 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
|
||||
return twErr
|
||||
}
|
||||
|
||||
func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclient.RegClient, r, rRefSrc ref.Ref, seen []string, rOpts []scheme.ReferrerOpts, tags []string) (*treeResult, error) {
|
||||
func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen []string, rOpts []scheme.ReferrerOpts, tags []string) (*treeResult, error) {
|
||||
tr := treeResult{
|
||||
Ref: r,
|
||||
}
|
||||
@ -1128,7 +1128,7 @@ func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclien
|
||||
}
|
||||
for _, d := range dl {
|
||||
rChild := r.SetDigest(d.Digest.String())
|
||||
tChild, err := artifactOpts.treeAddResult(ctx, rc, rChild, rRefSrc, seen, rOpts, tags)
|
||||
tChild, err := artifactOpts.treeAddResult(ctx, rc, rChild, seen, rOpts, tags)
|
||||
if tChild != nil {
|
||||
tChild.ArtifactType = d.ArtifactType
|
||||
if d.Platform != nil {
|
||||
@ -1149,10 +1149,16 @@ func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclien
|
||||
return &tr, fmt.Errorf("failed to check referrers for %s: %w", r.CommonName(), err)
|
||||
}
|
||||
if len(rl.Descriptors) > 0 {
|
||||
var rReferrer ref.Ref
|
||||
if rl.Source.IsSet() {
|
||||
rReferrer = rl.Source
|
||||
} else {
|
||||
rReferrer = rl.Subject
|
||||
}
|
||||
tr.Referrer = []*treeResult{}
|
||||
for _, d := range rl.Descriptors {
|
||||
rReferrer := rRefSrc.SetDigest(d.Digest.String())
|
||||
tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rReferrer, rRefSrc, seen, rOpts, tags)
|
||||
rReferrer = rReferrer.SetDigest(d.Digest.String())
|
||||
tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rReferrer, seen, rOpts, tags)
|
||||
if tReferrer != nil {
|
||||
tReferrer.ArtifactType = d.ArtifactType
|
||||
if d.Platform != nil {
|
||||
@ -1176,7 +1182,7 @@ func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclien
|
||||
for _, t := range tags {
|
||||
if strings.HasPrefix(t, prefix.Tag) && !sliceHasStr(rl.Tags, t) {
|
||||
rTag := r.SetTag(t)
|
||||
tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rTag, rRefSrc, seen, rOpts, tags)
|
||||
tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rTag, seen, rOpts, tags)
|
||||
if tReferrer != nil {
|
||||
tReferrer.Ref = tReferrer.Ref.SetTag(t)
|
||||
tr.Referrer = append(tr.Referrer, tReferrer)
|
||||
@ -1207,6 +1213,7 @@ type treeResult struct {
|
||||
ArtifactType string `json:"artifactType,omitempty"`
|
||||
Child []*treeResult `json:"child,omitempty"`
|
||||
Referrer []*treeResult `json:"referrer,omitempty"`
|
||||
ReferrerSrc ref.Ref `json:"referrerSource"`
|
||||
}
|
||||
|
||||
func (tr *treeResult) MarshalPretty() ([]byte, error) {
|
||||
|
25
referrer.go
25
referrer.go
@ -14,20 +14,20 @@ import (
|
||||
|
||||
// ReferrerList retrieves a list of referrers to a manifest.
|
||||
// The descriptor list should contain manifests that each have a subject field matching the requested ref.
|
||||
func (rc *RegClient) ReferrerList(ctx context.Context, r ref.Ref, opts ...scheme.ReferrerOpts) (referrer.ReferrerList, error) {
|
||||
if !r.IsSet() {
|
||||
return referrer.ReferrerList{}, fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
|
||||
func (rc *RegClient) ReferrerList(ctx context.Context, rSubject ref.Ref, opts ...scheme.ReferrerOpts) (referrer.ReferrerList, error) {
|
||||
if !rSubject.IsSet() {
|
||||
return referrer.ReferrerList{}, fmt.Errorf("ref is not set: %s%.0w", rSubject.CommonName(), errs.ErrInvalidReference)
|
||||
}
|
||||
// dedup warnings
|
||||
if w := warning.FromContext(ctx); w == nil {
|
||||
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
|
||||
}
|
||||
// resolve ref to a digest
|
||||
// set the digest on the subject reference
|
||||
config := scheme.ReferrerConfig{}
|
||||
for _, opt := range opts {
|
||||
opt(&config)
|
||||
}
|
||||
if r.Digest == "" || config.Platform != "" {
|
||||
if rSubject.Digest == "" || config.Platform != "" {
|
||||
mo := []ManifestOpts{WithManifestRequireDigest()}
|
||||
if config.Platform != "" {
|
||||
p, err := platform.Parse(config.Platform)
|
||||
@ -36,19 +36,22 @@ func (rc *RegClient) ReferrerList(ctx context.Context, r ref.Ref, opts ...scheme
|
||||
}
|
||||
mo = append(mo, WithManifestPlatform(p))
|
||||
}
|
||||
m, err := rc.ManifestHead(ctx, r, mo...)
|
||||
m, err := rc.ManifestHead(ctx, rSubject, mo...)
|
||||
if err != nil {
|
||||
return referrer.ReferrerList{}, fmt.Errorf("failed to get digest for subject: %w", err)
|
||||
}
|
||||
r = r.SetDigest(m.GetDescriptor().Digest.String())
|
||||
rSubject = rSubject.SetDigest(m.GetDescriptor().Digest.String())
|
||||
}
|
||||
// update for a new referrer source repo if requested
|
||||
if !config.SrcRepo.IsZero() {
|
||||
r = config.SrcRepo.SetDigest(r.Digest)
|
||||
// lookup the scheme for the appropriate ref
|
||||
var r ref.Ref
|
||||
if config.SrcRepo.IsSet() {
|
||||
r = config.SrcRepo
|
||||
} else {
|
||||
r = rSubject
|
||||
}
|
||||
schemeAPI, err := rc.schemeGet(r.Scheme)
|
||||
if err != nil {
|
||||
return referrer.ReferrerList{}, err
|
||||
}
|
||||
return schemeAPI.ReferrerList(ctx, r, opts...)
|
||||
return schemeAPI.ReferrerList(ctx, rSubject, opts...)
|
||||
}
|
||||
|
@ -124,12 +124,13 @@ func TestReferrerList(t *testing.T) {
|
||||
t.Fatalf("failed to generate refExt: %v", err)
|
||||
}
|
||||
tt := []struct {
|
||||
name string
|
||||
ref ref.Ref
|
||||
opts []scheme.ReferrerOpts
|
||||
count int
|
||||
firstAT string
|
||||
expectErr error
|
||||
name string
|
||||
ref ref.Ref
|
||||
opts []scheme.ReferrerOpts
|
||||
count int
|
||||
firstAT string
|
||||
expectSource ref.Ref
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
name: "resolve-tag",
|
||||
@ -161,8 +162,9 @@ func TestReferrerList(t *testing.T) {
|
||||
scheme.WithReferrerSource(refExt),
|
||||
scheme.WithReferrerMatchOpt(descriptor.MatchOpt{SortAnnotation: "preference"}),
|
||||
},
|
||||
count: 2,
|
||||
firstAT: "application/example.sbom",
|
||||
count: 2,
|
||||
firstAT: "application/example.sbom",
|
||||
expectSource: refExt,
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
@ -182,6 +184,15 @@ func TestReferrerList(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !ref.EqualRepository(rl.Subject, tc.ref) {
|
||||
t.Errorf("unexpected subject: expected %s, received %s", tc.ref.CommonName(), rl.Subject.CommonName())
|
||||
}
|
||||
if tc.expectSource.IsSet() && !ref.EqualRepository(rl.Source, tc.expectSource) {
|
||||
t.Errorf("unexpected source: expected %s, received %s", tc.expectSource.CommonName(), rl.Source.CommonName())
|
||||
}
|
||||
if tc.expectSource.IsZero() && !rl.Source.IsZero() {
|
||||
t.Errorf("source should not be set: received %s", rl.Source.CommonName())
|
||||
}
|
||||
if tc.count != len(rl.Descriptors) {
|
||||
t.Errorf("unexpected number of responses, expected %d, received response %v", tc.count, rl.Descriptors)
|
||||
}
|
||||
|
@ -22,18 +22,23 @@ func (o *OCIDir) ReferrerList(ctx context.Context, r ref.Ref, opts ...scheme.Ref
|
||||
return o.referrerList(ctx, r, opts...)
|
||||
}
|
||||
|
||||
func (o *OCIDir) referrerList(ctx context.Context, r ref.Ref, opts ...scheme.ReferrerOpts) (referrer.ReferrerList, error) {
|
||||
func (o *OCIDir) referrerList(ctx context.Context, rSubject ref.Ref, opts ...scheme.ReferrerOpts) (referrer.ReferrerList, error) {
|
||||
config := scheme.ReferrerConfig{}
|
||||
for _, opt := range opts {
|
||||
opt(&config)
|
||||
}
|
||||
var r ref.Ref
|
||||
if config.SrcRepo.IsSet() {
|
||||
r = config.SrcRepo.SetDigest(rSubject.Digest)
|
||||
} else {
|
||||
r = rSubject.SetDigest(rSubject.Digest)
|
||||
}
|
||||
rl := referrer.ReferrerList{
|
||||
Tags: []string{},
|
||||
}
|
||||
if r.Digest == "" {
|
||||
return rl, fmt.Errorf("digest required to query referrers %s", r.CommonName())
|
||||
if rSubject.Digest == "" {
|
||||
return rl, fmt.Errorf("digest required to query referrers %s", rSubject.CommonName())
|
||||
}
|
||||
rl.Subject = r
|
||||
|
||||
// pull referrer list by tag
|
||||
rlTag, err := referrer.FallbackTag(r)
|
||||
@ -60,6 +65,10 @@ func (o *OCIDir) referrerList(ctx context.Context, r ref.Ref, opts ...scheme.Ref
|
||||
return rl, fmt.Errorf("manifest is not an OCI index: %s", rlTag.CommonName())
|
||||
}
|
||||
// update referrer list
|
||||
rl.Subject = rSubject
|
||||
if config.SrcRepo.IsSet() {
|
||||
rl.Source = config.SrcRepo
|
||||
}
|
||||
rl.Manifest = m
|
||||
rl.Descriptors = ociML.Manifests
|
||||
rl.Annotations = ociML.Annotations
|
||||
|
@ -24,27 +24,31 @@ const OCISubjectHeader = "OCI-Subject"
|
||||
|
||||
// ReferrerList returns a list of referrers to a given reference.
|
||||
// The reference must include the digest. Use [regclient.ReferrerList] to resolve the platform or tag.
|
||||
func (reg *Reg) ReferrerList(ctx context.Context, r ref.Ref, opts ...scheme.ReferrerOpts) (referrer.ReferrerList, error) {
|
||||
func (reg *Reg) ReferrerList(ctx context.Context, rSubject ref.Ref, opts ...scheme.ReferrerOpts) (referrer.ReferrerList, error) {
|
||||
config := scheme.ReferrerConfig{}
|
||||
for _, opt := range opts {
|
||||
opt(&config)
|
||||
}
|
||||
var r ref.Ref
|
||||
if config.SrcRepo.IsSet() {
|
||||
r = config.SrcRepo.SetDigest(rSubject.Digest)
|
||||
} else {
|
||||
r = rSubject.SetDigest(rSubject.Digest)
|
||||
}
|
||||
rl := referrer.ReferrerList{
|
||||
Tags: []string{},
|
||||
}
|
||||
if r.Digest == "" {
|
||||
return rl, fmt.Errorf("digest required to query referrers %s", r.CommonName())
|
||||
if rSubject.Digest == "" {
|
||||
return rl, fmt.Errorf("digest required to query referrers %s", rSubject.CommonName())
|
||||
}
|
||||
// dedup warnings
|
||||
if w := warning.FromContext(ctx); w == nil {
|
||||
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
|
||||
}
|
||||
rl.Subject = r
|
||||
|
||||
found := false
|
||||
// try cache
|
||||
rCache := r.SetDigest(r.Digest)
|
||||
rl, err := reg.cacheRL.Get(rCache)
|
||||
rl, err := reg.cacheRL.Get(r)
|
||||
if err == nil {
|
||||
found = true
|
||||
}
|
||||
@ -61,7 +65,7 @@ func (reg *Reg) ReferrerList(ctx context.Context, r ref.Ref, opts ...scheme.Refe
|
||||
if err == nil {
|
||||
if config.MatchOpt.ArtifactType == "" {
|
||||
// only cache if successful and artifactType is not filtered
|
||||
reg.cacheRL.Set(rCache, rl)
|
||||
reg.cacheRL.Set(r, rl)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
@ -71,9 +75,13 @@ func (reg *Reg) ReferrerList(ctx context.Context, r ref.Ref, opts ...scheme.Refe
|
||||
if !found {
|
||||
rl, err = reg.referrerListByTag(ctx, r)
|
||||
if err == nil {
|
||||
reg.cacheRL.Set(rCache, rl)
|
||||
reg.cacheRL.Set(r, rl)
|
||||
}
|
||||
}
|
||||
rl.Subject = rSubject
|
||||
if config.SrcRepo.IsSet() {
|
||||
rl.Source = config.SrcRepo
|
||||
}
|
||||
if err != nil {
|
||||
return rl, err
|
||||
}
|
||||
|
@ -175,6 +175,7 @@ func WithReferrerSort(annotation string, desc bool) ReferrerOpts {
|
||||
func ReferrerFilter(config ReferrerConfig, rlIn referrer.ReferrerList) referrer.ReferrerList {
|
||||
return referrer.ReferrerList{
|
||||
Subject: rlIn.Subject,
|
||||
Source: rlIn.Source,
|
||||
Manifest: rlIn.Manifest,
|
||||
Annotations: rlIn.Annotations,
|
||||
Tags: rlIn.Tags,
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
// ReferrerList contains the response to a request for referrers to a subject
|
||||
type ReferrerList struct {
|
||||
Subject ref.Ref `json:"subject"` // subject queried
|
||||
Source ref.Ref `json:"source"` // source for referrers, if different from subject
|
||||
Descriptors []descriptor.Descriptor `json:"descriptors"` // descriptors found in Index
|
||||
Annotations map[string]string `json:"annotations,omitempty"` // annotations extracted from Index
|
||||
Manifest manifest.Manifest `json:"-"` // returned OCI Index
|
||||
@ -110,18 +111,21 @@ func (rl ReferrerList) IsEmpty() bool {
|
||||
func (rl ReferrerList) MarshalPretty() ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
tw := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
|
||||
if rl.Subject.Reference != "" {
|
||||
fmt.Fprintf(tw, "Subject:\t%s\n", rl.Subject.Reference)
|
||||
var rRef ref.Ref
|
||||
if rl.Subject.IsSet() {
|
||||
rRef = rl.Subject
|
||||
fmt.Fprintf(tw, "Subject:\t%s\n", rl.Subject.CommonName())
|
||||
}
|
||||
if rl.Source.IsSet() {
|
||||
rRef = rl.Source
|
||||
fmt.Fprintf(tw, "Source:\t%s\n", rl.Source.CommonName())
|
||||
}
|
||||
rRef := rl.Subject
|
||||
rRef.Tag = ""
|
||||
fmt.Fprintf(tw, "\t\n")
|
||||
fmt.Fprintf(tw, "Referrers:\t\n")
|
||||
for _, d := range rl.Descriptors {
|
||||
fmt.Fprintf(tw, "\t\n")
|
||||
if rRef.Reference != "" {
|
||||
rRef.Digest = d.Digest.String()
|
||||
fmt.Fprintf(tw, " Name:\t%s\n", rRef.CommonName())
|
||||
if rRef.IsSet() {
|
||||
fmt.Fprintf(tw, " Name:\t%s\n", rRef.SetDigest(d.Digest.String()).CommonName())
|
||||
}
|
||||
err := d.MarshalPrettyTW(tw, " ")
|
||||
if err != nil {
|
||||
|
@ -2,6 +2,7 @@ package referrer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
@ -11,6 +12,7 @@ import (
|
||||
"github.com/regclient/regclient/types/manifest"
|
||||
"github.com/regclient/regclient/types/mediatype"
|
||||
v1 "github.com/regclient/regclient/types/oci/v1"
|
||||
"github.com/regclient/regclient/types/ref"
|
||||
)
|
||||
|
||||
const bOCIImg = `
|
||||
@ -359,3 +361,62 @@ func TestDelete(t *testing.T) {
|
||||
t.Errorf("number of descriptors, expected 0, received %d", len(rl.Descriptors))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
rl := &ReferrerList{
|
||||
Descriptors: []descriptor.Descriptor{},
|
||||
Annotations: map[string]string{},
|
||||
Tags: []string{},
|
||||
}
|
||||
outB, err := rl.MarshalPretty()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal empty referrer list: %v", err)
|
||||
}
|
||||
out := string(outB)
|
||||
if strings.Contains(out, "Subject:") {
|
||||
t.Errorf("empty response contains a subject line: %s", out)
|
||||
}
|
||||
if strings.Contains(out, "Source:") {
|
||||
t.Errorf("empty response contains a source line: %s", out)
|
||||
}
|
||||
if strings.Contains(out, "Annotations:") {
|
||||
t.Errorf("empty response contains an annotations line: %s", out)
|
||||
}
|
||||
|
||||
rSubj, err := ref.New("registry.example.org/test/subject:latest")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse subject ref: %v", err)
|
||||
}
|
||||
rSource, err := ref.New("registry.example.com/test/external")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse source ref: %v", err)
|
||||
}
|
||||
rl = &ReferrerList{
|
||||
Subject: rSubj,
|
||||
Source: rSource,
|
||||
Descriptors: []descriptor.Descriptor{
|
||||
dOCIImg,
|
||||
dOCIImgAT,
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"com.example.test": "test annotation",
|
||||
},
|
||||
Tags: []string{},
|
||||
}
|
||||
outB, err = rl.MarshalPretty()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal empty referrer list: %v", err)
|
||||
}
|
||||
out = string(outB)
|
||||
if !strings.Contains(out, "Subject:") {
|
||||
t.Errorf("empty response is missing a subject line: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Source:") {
|
||||
t.Errorf("empty response is missing a source line: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Annotations:") {
|
||||
t.Errorf("empty response is missing an annotations line: %s", out)
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user