1
0
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:
Brandon Mitchell 2024-12-03 17:12:10 -05:00
parent d73d40b126
commit 763599514d
No known key found for this signature in database
GPG Key ID: 6E0FF28C767A8BEE
8 changed files with 148 additions and 44 deletions

View File

@ -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) {

View File

@ -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...)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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 {

View File

@ -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)
}
}