mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 12:58:02 +08:00
feat(sync): add SyncLegacyCosignTags config to skip syncing legacy cosign/SBOM tags when disabled (#3842)
* feat(sync): add SyncLegacyCosignTags config to skip syncing legacy cosign/SBOM tags when disabled Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> * fix: sync on demand with referrers API should not use recursion to sync referrers of referrers Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> * fix: add tests SyncLegacyCosignTags and changes in /referrers on demand sync Credit for the tests goes to @jzhn see: https://github.com/project-zot/zot/pull/3840/changes Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> * fix: remove redundant syncRef logic which synced referrers both with the zot inner() implementation and with regctl native implementation Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> --------- Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
This commit is contained in:
@@ -32,12 +32,19 @@ type RegistryConfig struct {
|
||||
MaxRetries *int
|
||||
RetryDelay *time.Duration
|
||||
OnlySigned *bool
|
||||
SyncLegacyCosignTags *bool // when unset, defaults to true
|
||||
CredentialHelper string
|
||||
PreserveDigest bool // sync without converting
|
||||
SyncTimeout time.Duration // overall HTTP client timeout for all sync operations
|
||||
ResponseHeaderTimeout time.Duration `yaml:"-"` // response header timeout; set in root.go
|
||||
}
|
||||
|
||||
// ShouldSyncLegacyCosignTags returns whether to sync legacy cosign tags (e.g. sha256-<digest>.sig/sbom).
|
||||
// Default is true when SyncLegacyCosignTags is unset (nil).
|
||||
func (r RegistryConfig) ShouldSyncLegacyCosignTags() bool {
|
||||
return r.SyncLegacyCosignTags == nil || *r.SyncLegacyCosignTags
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Prefix string
|
||||
Tags *Tags
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package sync_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
syncconf "zotregistry.dev/zot/v2/pkg/extensions/config/sync"
|
||||
)
|
||||
|
||||
func TestRegistryConfig_ShouldSyncLegacyCosignTags(t *testing.T) {
|
||||
Convey("ShouldSyncLegacyCosignTags", t, func() {
|
||||
Convey("returns true when SyncLegacyCosignTags is nil (default)", func() {
|
||||
cfg := syncconf.RegistryConfig{}
|
||||
So(cfg.SyncLegacyCosignTags, ShouldBeNil)
|
||||
So(cfg.ShouldSyncLegacyCosignTags(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("returns true when SyncLegacyCosignTags is true", func() {
|
||||
v := true
|
||||
cfg := syncconf.RegistryConfig{SyncLegacyCosignTags: &v}
|
||||
So(cfg.ShouldSyncLegacyCosignTags(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("returns false when SyncLegacyCosignTags is false", func() {
|
||||
v := false
|
||||
cfg := syncconf.RegistryConfig{SyncLegacyCosignTags: &v}
|
||||
So(cfg.ShouldSyncLegacyCosignTags(), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -343,12 +343,16 @@ func (service *BaseService) SyncReferrers(ctx context.Context, repo string,
|
||||
service.log.Info().Str("remote", remoteURL).Str("repository", repo).Str("subject", subjectDigestStr).
|
||||
Interface("reference types", referenceTypes).Msg("syncing reference for image")
|
||||
|
||||
tags, err := service.getTags(ctx, remoteRepo, false)
|
||||
if err != nil {
|
||||
service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).
|
||||
Err(err).Msg("error while getting tags for repo")
|
||||
var tags []string
|
||||
if service.config.ShouldSyncLegacyCosignTags() {
|
||||
var err error
|
||||
tags, err = service.getTags(ctx, remoteRepo, false)
|
||||
if err != nil {
|
||||
service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).
|
||||
Err(err).Msg("error while getting tags for repo")
|
||||
|
||||
return err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
remoteImageRef, err := service.remote.GetImageReference(remoteRepo, subjectDigestStr)
|
||||
@@ -367,7 +371,7 @@ func (service *BaseService) SyncReferrers(ctx context.Context, repo string,
|
||||
return err
|
||||
}
|
||||
|
||||
if err := service.syncReferrers(ctx, tags, repo, remoteRepo, localImageRef, remoteImageRef); err != nil {
|
||||
if err := service.syncReferrers(ctx, tags, repo, remoteRepo, localImageRef, remoteImageRef, false); err != nil {
|
||||
service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
|
||||
Str("repo", repo).Str("reference", subjectDigestStr).Msg("failed to sync referrers")
|
||||
|
||||
@@ -430,7 +434,7 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// skip referrers, they are synced in syncTagAndReferrers.
|
||||
// skip referrers tags and cosign tags here; they are synced via syncReferrers in syncImage.
|
||||
if common.IsCosignTag(tag) || common.IsReferrersTag(tag) {
|
||||
continue
|
||||
}
|
||||
@@ -460,7 +464,7 @@ func (service *BaseService) SyncRepo(ctx context.Context, repo string) error {
|
||||
}
|
||||
|
||||
func (service *BaseService) syncRef(ctx context.Context, localRepo string, remoteImageRef, localImageRef ref.Ref,
|
||||
remoteDigest godigest.Digest, recursive bool,
|
||||
remoteDigest godigest.Digest,
|
||||
) (bool, error) {
|
||||
var reference string
|
||||
|
||||
@@ -475,9 +479,6 @@ func (service *BaseService) syncRef(ctx context.Context, localRepo string, remot
|
||||
}
|
||||
|
||||
copyOpts := []regclient.ImageOpts{}
|
||||
if recursive {
|
||||
copyOpts = append(copyOpts, regclient.ImageWithReferrers())
|
||||
}
|
||||
|
||||
// check if image is already synced
|
||||
skipImage, err = service.destination.CanSkipImage(localRepo, reference, remoteDigest)
|
||||
@@ -570,17 +571,6 @@ func (service *BaseService) syncImage(ctx context.Context, localRepo, remoteRepo
|
||||
|
||||
// if onlySigned flag true in config and the image is not itself a signature
|
||||
if checkIsSigned {
|
||||
// if need tags for checking signature (onlySigned option true) or needs for referrers
|
||||
if len(repoTags) == 0 {
|
||||
repoTags, err = service.getTags(ctx, remoteRepo, false)
|
||||
if err != nil {
|
||||
service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", remoteRepo).
|
||||
Err(err).Msg("error while getting tags for repo")
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
referrers, err := service.rc.ReferrerList(ctx, remoteImageRef)
|
||||
if err != nil {
|
||||
service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", remoteRepo).
|
||||
@@ -589,11 +579,24 @@ func (service *BaseService) syncImage(ctx context.Context, localRepo, remoteRepo
|
||||
return err
|
||||
}
|
||||
|
||||
// verify repo contains a cosign signature for this manifest
|
||||
hasCosignSignature := slices.Contains(repoTags, fmt.Sprintf("%s-%s.sig", remoteDigest.Algorithm(),
|
||||
remoteDigest.Encoded()))
|
||||
isSigned := hasSignatureReferrers(referrers)
|
||||
if service.config.ShouldSyncLegacyCosignTags() {
|
||||
// legacy fallback: verify repo contains a cosign signature tag for this manifest
|
||||
if len(repoTags) == 0 {
|
||||
repoTags, err = service.getTags(ctx, remoteRepo, false)
|
||||
if err != nil {
|
||||
service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", remoteRepo).
|
||||
Err(err).Msg("error while getting tags for repo")
|
||||
|
||||
isSigned := hasSignatureReferrers(referrers) || hasCosignSignature
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hasCosignSignature := slices.Contains(repoTags, fmt.Sprintf("%s-%s.sig", remoteDigest.Algorithm(),
|
||||
remoteDigest.Encoded()))
|
||||
|
||||
isSigned = isSigned || hasCosignSignature
|
||||
}
|
||||
if !isSigned {
|
||||
// skip unsigned images
|
||||
service.log.Info().Str("image", remoteImageRef.CommonName()).
|
||||
@@ -617,13 +620,13 @@ func (service *BaseService) syncImage(ctx context.Context, localRepo, remoteRepo
|
||||
defer service.destination.CleanupImage(localImageRef, localRepo) //nolint: errcheck
|
||||
|
||||
// first sync image
|
||||
skipped, err := service.syncRef(ctx, localRepo, remoteImageRef, localImageRef, localDigest, false)
|
||||
skipped, err := service.syncRef(ctx, localRepo, remoteImageRef, localImageRef, localDigest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if withReferrers {
|
||||
_ = service.syncReferrers(ctx, repoTags, localRepo, remoteRepo, localImageRef, remoteImageRef)
|
||||
_ = service.syncReferrers(ctx, repoTags, localRepo, remoteRepo, localImageRef, remoteImageRef, true)
|
||||
}
|
||||
|
||||
// convert image to oci if needed
|
||||
@@ -681,15 +684,15 @@ func (service *BaseService) getTags(ctx context.Context, repo string, noCache bo
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// syncs all referrers recursively.
|
||||
// syncs referrers of the given subject, when recursive is true also syncs referrers of those referrers.
|
||||
func (service *BaseService) syncReferrers(ctx context.Context, tags []string, localRepo, remoteRepo string,
|
||||
localImageRef ref.Ref, remoteImageRef ref.Ref,
|
||||
localImageRef ref.Ref, remoteImageRef ref.Ref, recursive bool,
|
||||
) error {
|
||||
seen := []string{}
|
||||
|
||||
var err error
|
||||
|
||||
if len(tags) == 0 {
|
||||
if service.config.ShouldSyncLegacyCosignTags() && len(tags) == 0 {
|
||||
tags, err = service.getTags(ctx, remoteRepo, false)
|
||||
if err != nil {
|
||||
service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", remoteRepo).
|
||||
@@ -700,11 +703,11 @@ func (service *BaseService) syncReferrers(ctx context.Context, tags []string, lo
|
||||
}
|
||||
|
||||
var inner func(ctx context.Context, tags []string, localRepo, remoteRepo string,
|
||||
localImageRef ref.Ref, remoteImageRef ref.Ref, seen []string,
|
||||
localImageRef ref.Ref, remoteImageRef ref.Ref, seen []string, recursive bool,
|
||||
) error
|
||||
|
||||
inner = func(ctx context.Context, tags []string, localRepo, remoteRepo string,
|
||||
localImageRef ref.Ref, remoteImageRef ref.Ref, seen []string,
|
||||
localImageRef ref.Ref, remoteImageRef ref.Ref, seen []string, recursive bool,
|
||||
) error {
|
||||
var err error
|
||||
|
||||
@@ -737,39 +740,45 @@ func (service *BaseService) syncReferrers(ctx context.Context, tags []string, lo
|
||||
|
||||
localImageRef = localImageRef.SetDigest(desc.Digest.String())
|
||||
|
||||
_, err := service.syncRef(ctx, localRepo, remoteImageRef, localImageRef, desc.Digest, false)
|
||||
_, err := service.syncRef(ctx, localRepo, remoteImageRef, localImageRef, desc.Digest)
|
||||
if err != nil {
|
||||
service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
|
||||
Str("repo", localRepo).Str("local reference", localImageRef.Tag).
|
||||
Str("remote reference", remoteImageRef.Tag).Msg("failed to sync referrer")
|
||||
}
|
||||
|
||||
_ = inner(ctx, tags, localRepo, remoteRepo, localImageRef, remoteImageRef, seen)
|
||||
if recursive {
|
||||
_ = inner(ctx, tags, localRepo, remoteRepo, localImageRef, remoteImageRef, seen, recursive)
|
||||
}
|
||||
}
|
||||
|
||||
// try cosign
|
||||
prefix := fmt.Sprintf("%s-%s.", remoteDigest.Algorithm(), remoteDigest.Encoded())
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(tag, prefix) {
|
||||
remoteImageRef = remoteImageRef.SetTag(tag)
|
||||
if service.config.ShouldSyncLegacyCosignTags() {
|
||||
// try legacy cosign-style tags (sha256-<subjectDigest>.<suffix>)
|
||||
prefix := fmt.Sprintf("%s-%s.", remoteDigest.Algorithm(), remoteDigest.Encoded())
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(tag, prefix) {
|
||||
remoteImageRef = remoteImageRef.SetTag(tag)
|
||||
|
||||
localImageRef = localImageRef.SetTag(tag)
|
||||
localImageRef = localImageRef.SetTag(tag)
|
||||
|
||||
_, err := service.syncRef(ctx, localRepo, remoteImageRef, localImageRef, remoteDigest, true)
|
||||
if err != nil {
|
||||
service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
|
||||
Str("repo", localRepo).Str("local reference", localImageRef.Tag).
|
||||
Str("remote reference", remoteImageRef.Tag).Msg("failed to sync referrer")
|
||||
_, err := service.syncRef(ctx, localRepo, remoteImageRef, localImageRef, remoteDigest)
|
||||
if err != nil {
|
||||
service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
|
||||
Str("repo", localRepo).Str("local reference", localImageRef.Tag).
|
||||
Str("remote reference", remoteImageRef.Tag).Msg("failed to sync referrer")
|
||||
}
|
||||
|
||||
if recursive {
|
||||
_ = inner(ctx, tags, localRepo, remoteRepo, localImageRef, remoteImageRef, seen, recursive)
|
||||
}
|
||||
}
|
||||
|
||||
_ = inner(ctx, tags, localRepo, remoteRepo, localImageRef, remoteImageRef, seen)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return inner(ctx, tags, localRepo, remoteRepo, localImageRef, remoteImageRef, seen)
|
||||
return inner(ctx, tags, localRepo, remoteRepo, localImageRef, remoteImageRef, seen, recursive)
|
||||
}
|
||||
|
||||
func (service *BaseService) ResetCatalog() {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/regclient/regclient"
|
||||
"github.com/regclient/regclient/types/ref"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
@@ -93,6 +94,8 @@ func TestService(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("test SyncReferrers ReferrerList error", t, func() {
|
||||
// SyncReferrers is the on-demand API; it calls syncReferrers(..., recursive=false)
|
||||
// so only direct referrers are synced (no referrers-of-referrers).
|
||||
conf := syncconf.RegistryConfig{
|
||||
URLs: []string{"http://localhost"},
|
||||
}
|
||||
@@ -250,7 +253,13 @@ func TestService(t *testing.T) {
|
||||
// Create an invalid remote reference that will cause ReferrerList to fail with "ref is not set" error
|
||||
remoteImageRef := ref.Ref{}
|
||||
|
||||
err = service.syncReferrers(ctx, []string{"tag"}, "localrepo", "remoterepo", localImageRef, remoteImageRef)
|
||||
err = service.syncReferrers(ctx, []string{"tag"}, "localrepo", "remoterepo", localImageRef, remoteImageRef, false)
|
||||
|
||||
// The error should be "ref is not set" as defined in regclient ReferrerList function
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err.Error(), ShouldContainSubstring, "ref is not set")
|
||||
|
||||
err = service.syncReferrers(ctx, []string{"tag"}, "localrepo", "remoterepo", localImageRef, remoteImageRef, true)
|
||||
|
||||
// The error should be "ref is not set" as defined in regclient ReferrerList function
|
||||
So(err, ShouldNotBeNil)
|
||||
@@ -635,6 +644,140 @@ func TestService(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncLegacyCosignTagsSyncReferrers(t *testing.T) {
|
||||
Convey("SyncLegacyCosignTags=false skips getTags and the digest-tag loop in SyncReferrers", t, func() {
|
||||
getTagsCallCount := 0
|
||||
syncLegacyFalse := false
|
||||
|
||||
conf := syncconf.RegistryConfig{
|
||||
URLs: []string{"http://localhost"},
|
||||
SyncLegacyCosignTags: &syncLegacyFalse,
|
||||
}
|
||||
|
||||
service, err := New(conf, "", nil, t.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.NewTestLogger())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
service.rc = regclient.New()
|
||||
|
||||
mockRemote := &mocks.SyncRemoteMock{
|
||||
GetTagsFn: func(ctx context.Context, repo string) ([]string, error) {
|
||||
getTagsCallCount++
|
||||
return nil, errors.New("getTags must not be called when SyncLegacyCosignTags=false")
|
||||
},
|
||||
GetImageReferenceFn: func(repo, tag string) (ref.Ref, error) {
|
||||
return ref.New(repo + "@" + tag)
|
||||
},
|
||||
GetDigestFn: func(ctx context.Context, repo, tag string) (godigest.Digest, error) {
|
||||
return godigest.Digest("sha256:" + strings.Repeat("a", 64)), nil
|
||||
},
|
||||
}
|
||||
service.remote = mockRemote
|
||||
|
||||
mockDest := &mocks.SyncDestinationMock{
|
||||
GetImageReferenceFn: func(repo, tag string) (ref.Ref, error) {
|
||||
return ref.New("local/" + repo + "@" + tag)
|
||||
},
|
||||
CommitAllFn: func(repo string, imageReference ref.Ref) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
service.destination = mockDest
|
||||
|
||||
ctx := context.Background()
|
||||
digest := "sha256:" + strings.Repeat("a", 64)
|
||||
|
||||
err = service.SyncReferrers(ctx, "repo", digest, nil)
|
||||
// We expect an error from rc.ReferrerList since it's not connected to a registry,
|
||||
// but what we really care about is that getTags was NOT called.
|
||||
So(getTagsCallCount, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("SyncLegacyCosignTags=true (default) calls getTags in SyncReferrers", t, func() {
|
||||
getTagsCallCount := 0
|
||||
syncLegacyTrue := true
|
||||
|
||||
conf := syncconf.RegistryConfig{
|
||||
URLs: []string{"http://localhost"},
|
||||
SyncLegacyCosignTags: &syncLegacyTrue,
|
||||
}
|
||||
|
||||
service, err := New(conf, "", nil, t.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.NewTestLogger())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
service.rc = regclient.New()
|
||||
|
||||
mockRemote := &mocks.SyncRemoteMock{
|
||||
GetTagsFn: func(ctx context.Context, repo string) ([]string, error) {
|
||||
getTagsCallCount++
|
||||
return []string{"tag1"}, nil
|
||||
},
|
||||
GetImageReferenceFn: func(repo, tag string) (ref.Ref, error) {
|
||||
return ref.New(repo + "@" + tag)
|
||||
},
|
||||
}
|
||||
service.remote = mockRemote
|
||||
|
||||
mockDest := &mocks.SyncDestinationMock{
|
||||
GetImageReferenceFn: func(repo, tag string) (ref.Ref, error) {
|
||||
return ref.New("local/" + repo + "@" + tag)
|
||||
},
|
||||
}
|
||||
service.destination = mockDest
|
||||
|
||||
ctx := context.Background()
|
||||
digest := "sha256:" + strings.Repeat("a", 64)
|
||||
|
||||
err = service.SyncReferrers(ctx, "repo", digest, nil)
|
||||
// We don't care if it fails, only that getTags was called.
|
||||
So(getTagsCallCount, ShouldEqual, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnDemandSyncReferrersNonRecursive(t *testing.T) {
|
||||
Convey("SyncReferrers (on-demand) uses non-recursive sync", t, func() {
|
||||
// Verify that recursive calls are not made.
|
||||
// We use a non-nil regclient to avoid panics.
|
||||
syncLegacyFalse := false
|
||||
|
||||
conf := syncconf.RegistryConfig{
|
||||
URLs: []string{"http://localhost"},
|
||||
SyncLegacyCosignTags: &syncLegacyFalse,
|
||||
}
|
||||
|
||||
service, err := New(conf, "", nil, t.TempDir(), storage.StoreController{}, mocks.MetaDBMock{}, log.NewTestLogger())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
service.rc = regclient.New()
|
||||
|
||||
mockRemote := &mocks.SyncRemoteMock{
|
||||
GetImageReferenceFn: func(repo, tag string) (ref.Ref, error) {
|
||||
return ref.New(repo + "@" + tag)
|
||||
},
|
||||
GetDigestFn: func(ctx context.Context, repo, tag string) (godigest.Digest, error) {
|
||||
return godigest.Digest("sha256:" + strings.Repeat("a", 64)), nil
|
||||
},
|
||||
GetTagsFn: func(ctx context.Context, repo string) ([]string, error) {
|
||||
return []string{}, nil
|
||||
},
|
||||
}
|
||||
service.remote = mockRemote
|
||||
|
||||
mockDest := &mocks.SyncDestinationMock{
|
||||
GetImageReferenceFn: func(repo, tag string) (ref.Ref, error) {
|
||||
return ref.New("local/" + repo + "@" + tag)
|
||||
},
|
||||
}
|
||||
service.destination = mockDest
|
||||
|
||||
ctx := context.Background()
|
||||
digest := "sha256:" + strings.Repeat("a", 64)
|
||||
|
||||
err = service.SyncReferrers(ctx, "repo", digest, nil)
|
||||
// It will fail because rc is not connected, but it shouldn't panic.
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDestinationRegistry(t *testing.T) {
|
||||
Convey("make StoreController", t, func() {
|
||||
dir := t.TempDir()
|
||||
|
||||
@@ -5425,6 +5425,304 @@ func TestSyncedSignaturesMetaDB(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncLegacyCosignTags(t *testing.T) {
|
||||
Convey("SyncLegacyCosignTags controls whether legacy cosign/SBOM tags are synced", t, func() {
|
||||
sctlr, srcBaseURL, srcDir, _ := makeUpstreamServer(t, false, false)
|
||||
scm := test.NewControllerManager(sctlr)
|
||||
scm.StartAndWait(sctlr.Config.HTTP.Port)
|
||||
defer scm.StopServer()
|
||||
|
||||
// Get image digest and add legacy SBOM tag on source (sha256-<digest>.sbom)
|
||||
resp, err := resty.R().Get(srcBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
imageDigestStr := resp.Header().Get("Docker-Content-Digest")
|
||||
So(imageDigestStr, ShouldNotBeEmpty)
|
||||
imageDigest, err := godigest.Parse(imageDigestStr)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
srcPort := getPortFromBaseURL(srcBaseURL)
|
||||
attachSBOM(srcDir, srcPort, testImage, imageDigest)
|
||||
|
||||
legacyTag := imageDigest.Algorithm().String() + "-" + imageDigest.Encoded() + ".sbom"
|
||||
|
||||
tlsVerify := false
|
||||
regex := ".*"
|
||||
semver := false
|
||||
|
||||
Convey("With SyncLegacyCosignTags true (default), legacy tag is synced", func() {
|
||||
syncLegacyTrue := true
|
||||
syncRegistryConfig := syncconf.RegistryConfig{
|
||||
Content: []syncconf.Content{
|
||||
{Prefix: "**", Tags: &syncconf.Tags{Regex: ®ex, Semver: &semver}},
|
||||
},
|
||||
URLs: []string{srcBaseURL},
|
||||
TLSVerify: &tlsVerify,
|
||||
OnDemand: true,
|
||||
SyncLegacyCosignTags: &syncLegacyTrue,
|
||||
}
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
dctlr, destBaseURL, destDir, destClient := makeDownstreamServer(t, false, syncConfig)
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
defer dcm.StopServer()
|
||||
|
||||
// Trigger image sync
|
||||
resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
// Trigger referrers sync (pulls legacy tags when SyncLegacyCosignTags is true)
|
||||
resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/referrers/" + imageDigestStr)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// Verify via storage (avoid GET manifest for legacy tag, which can trigger on-demand sync)
|
||||
indexPath := path.Join(destDir, testImage, "index.json")
|
||||
found := false
|
||||
|
||||
for start := time.Now(); time.Since(start) < 30*time.Second; time.Sleep(250 * time.Millisecond) {
|
||||
indexBuf, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var destIndex ispec.Index
|
||||
if json.Unmarshal(indexBuf, &destIndex) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, desc := range destIndex.Manifests {
|
||||
if tag, ok := desc.Annotations[ispec.AnnotationRefName]; ok && tag == legacyTag {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatalf("legacy tag %q should be present on destination index", legacyTag)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("With SyncLegacyCosignTags false, legacy tag is not synced", func() {
|
||||
syncLegacyFalse := false
|
||||
pollInterval, _ := time.ParseDuration("5s")
|
||||
syncRegistryConfig := syncconf.RegistryConfig{
|
||||
Content: []syncconf.Content{
|
||||
{Prefix: "**", Tags: &syncconf.Tags{Regex: ®ex, Semver: &semver}},
|
||||
},
|
||||
URLs: []string{srcBaseURL},
|
||||
TLSVerify: &tlsVerify,
|
||||
OnDemand: false,
|
||||
PollInterval: pollInterval,
|
||||
SyncLegacyCosignTags: &syncLegacyFalse,
|
||||
}
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
dctlr, destBaseURL, destDir, destClient := makeDownstreamServer(t, false, syncConfig)
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
defer dcm.StopServer()
|
||||
|
||||
// Wait for periodic sync to complete (do not call GET manifest for legacy tag to avoid on-demand sync)
|
||||
found, err := test.ReadLogFileAndSearchString(dctlr.Config.Log.Output, "finished syncing repo", 30*time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
// Poll until testImage repo exists on destination (periodic sync may sync multiple repos)
|
||||
var resp *resty.Response
|
||||
for start := time.Now(); time.Since(start) < 30*time.Second; time.Sleep(500 * time.Millisecond) {
|
||||
resp, err = destClient.R().Get(destBaseURL + "/v2/" + testImage + "/tags/list")
|
||||
if err == nil && resp != nil && resp.StatusCode() == http.StatusOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// Verify via tags/list: legacy tag must not be present
|
||||
var destTags TagsList
|
||||
So(json.Unmarshal(resp.Body(), &destTags), ShouldBeNil)
|
||||
So(destTags.Tags, ShouldNotContain, legacyTag)
|
||||
|
||||
// Verify via storage: index.json must not reference the legacy tag
|
||||
indexPath := path.Join(destDir, testImage, "index.json")
|
||||
indexBuf, err := os.ReadFile(indexPath)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var destIndex ispec.Index
|
||||
So(json.Unmarshal(indexBuf, &destIndex), ShouldBeNil)
|
||||
|
||||
for _, desc := range destIndex.Manifests {
|
||||
if tag, ok := desc.Annotations[ispec.AnnotationRefName]; ok && tag == legacyTag {
|
||||
t.Fatalf("legacy tag %q should not be in destination index", legacyTag)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncLegacyCosignTagsWithSignatures(t *testing.T) {
|
||||
Convey("SyncLegacyCosignTags controls whether legacy cosign signature tags (.sig) are synced", t, func() {
|
||||
sctlr, srcBaseURL, _, _ := makeUpstreamServer(t, false, false)
|
||||
scm := test.NewControllerManager(sctlr)
|
||||
scm.StartAndWait(sctlr.Config.HTTP.Port)
|
||||
defer scm.StopServer()
|
||||
|
||||
// Push image and sign it (adds legacy .sig tag on source)
|
||||
var digest godigest.Digest
|
||||
So(func() { digest = pushRepo(srcBaseURL, testSignedImage) }, ShouldNotPanic)
|
||||
srcPort := getPortFromBaseURL(srcBaseURL)
|
||||
cwd, err := os.Getwd()
|
||||
So(err, ShouldBeNil)
|
||||
defer func() { _ = os.Chdir(cwd) }()
|
||||
tdir := t.TempDir()
|
||||
_ = os.Chdir(tdir)
|
||||
generateKeyPairs(tdir)
|
||||
So(func() { signImage(tdir, srcPort, testSignedImage, digest) }, ShouldNotPanic)
|
||||
|
||||
legacySigTag := digest.Algorithm().String() + "-" + digest.Encoded() + ".sig"
|
||||
imageDigestStr := digest.String()
|
||||
tlsVerify := false
|
||||
regex := ".*"
|
||||
semver := false
|
||||
|
||||
Convey("With SyncLegacyCosignTags true, legacy signature tag is synced", func() {
|
||||
syncLegacyTrue := true
|
||||
syncRegistryConfig := syncconf.RegistryConfig{
|
||||
Content: []syncconf.Content{
|
||||
{Prefix: "**", Tags: &syncconf.Tags{Regex: ®ex, Semver: &semver}},
|
||||
},
|
||||
URLs: []string{srcBaseURL},
|
||||
TLSVerify: &tlsVerify,
|
||||
OnDemand: true,
|
||||
SyncLegacyCosignTags: &syncLegacyTrue,
|
||||
}
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
dctlr, destBaseURL, destDir, destClient := makeDownstreamServer(t, false, syncConfig)
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
defer dcm.StopServer()
|
||||
|
||||
// Trigger image sync
|
||||
resp, err := destClient.R().Get(destBaseURL + "/v2/" + testSignedImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
// Trigger referrers sync (pulls legacy .sig when SyncLegacyCosignTags is true)
|
||||
resp, err = destClient.R().Get(destBaseURL + "/v2/" + testSignedImage + "/referrers/" + imageDigestStr)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// Verify via storage (avoid GET manifest for legacy tag, which can trigger on-demand sync)
|
||||
indexPath := path.Join(destDir, testSignedImage, "index.json")
|
||||
found := false
|
||||
|
||||
for start := time.Now(); time.Since(start) < 30*time.Second; time.Sleep(250 * time.Millisecond) {
|
||||
indexBuf, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var destIndex ispec.Index
|
||||
if json.Unmarshal(indexBuf, &destIndex) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, desc := range destIndex.Manifests {
|
||||
if tag, ok := desc.Annotations[ispec.AnnotationRefName]; ok && tag == legacySigTag {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatalf("legacy signature tag %q should be present on destination index", legacySigTag)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("With SyncLegacyCosignTags false, legacy signature tag is not synced", func() {
|
||||
syncLegacyFalse := false
|
||||
pollInterval, _ := time.ParseDuration("5s")
|
||||
syncRegistryConfig := syncconf.RegistryConfig{
|
||||
Content: []syncconf.Content{
|
||||
{Prefix: "**", Tags: &syncconf.Tags{Regex: ®ex, Semver: &semver}},
|
||||
},
|
||||
URLs: []string{srcBaseURL},
|
||||
TLSVerify: &tlsVerify,
|
||||
OnDemand: false,
|
||||
PollInterval: pollInterval,
|
||||
SyncLegacyCosignTags: &syncLegacyFalse,
|
||||
}
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
dctlr, destBaseURL, destDir, destClient := makeDownstreamServer(t, false, syncConfig)
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
defer dcm.StopServer()
|
||||
|
||||
// Wait for periodic sync to complete for this repo (do not call GET manifest for legacy tag to avoid on-demand sync)
|
||||
found, err := test.ReadLogFileAndSearchString(dctlr.Config.Log.Output, "finished syncing repo", 30*time.Second)
|
||||
So(err, ShouldBeNil)
|
||||
So(found, ShouldBeTrue)
|
||||
// Poll until signed-repo exists on destination (periodic sync may sync multiple repos)
|
||||
var resp *resty.Response
|
||||
for start := time.Now(); time.Since(start) < 30*time.Second; time.Sleep(500 * time.Millisecond) {
|
||||
resp, err = destClient.R().Get(destBaseURL + "/v2/" + testSignedImage + "/tags/list")
|
||||
if err == nil && resp != nil && resp.StatusCode() == http.StatusOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// Verify via tags/list: legacy signature tag must not be present
|
||||
var destTags TagsList
|
||||
So(json.Unmarshal(resp.Body(), &destTags), ShouldBeNil)
|
||||
So(destTags.Tags, ShouldNotContain, legacySigTag)
|
||||
|
||||
// Verify via storage: index.json must not reference the legacy signature tag
|
||||
indexPath := path.Join(destDir, testSignedImage, "index.json")
|
||||
indexBuf, err := os.ReadFile(indexPath)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var destIndex ispec.Index
|
||||
So(json.Unmarshal(indexBuf, &destIndex), ShouldBeNil)
|
||||
|
||||
for _, desc := range destIndex.Manifests {
|
||||
if tag, ok := desc.Annotations[ispec.AnnotationRefName]; ok && tag == legacySigTag {
|
||||
t.Fatalf("legacy signature tag %q should not be in destination index", legacySigTag)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnDemandRetryGoroutine(t *testing.T) {
|
||||
Convey("Verify ondemand sync retries in background on error", t, func() {
|
||||
srcPort := test.GetFreePort()
|
||||
@@ -7501,6 +7799,343 @@ func TestECRCredentialsHelper(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestOnDemandReferrerSyncFlags verifies SyncLegacyCosignTags and on-demand
|
||||
// non-recursive referrer sync in a full upstream/downstream server setup.
|
||||
//
|
||||
// Topology pushed to the upstream:
|
||||
//
|
||||
// testImage:testImageTag (base image)
|
||||
// ├── oci-referrer (OCI-spec referrer — attached via subject field)
|
||||
// └── sha256-<hex>.sig (legacy cosign-style digest-encoded tag)
|
||||
//
|
||||
// With SyncLegacyCosignTags=false, referrers from the referrers API are synced,
|
||||
// but the legacy .sig tag is not; the .sig tag must not appear on the downstream.
|
||||
// On-demand SyncReferrers never recurses (only direct referrers are synced);
|
||||
// recursive referrer sync is done only in periodic sync.
|
||||
func TestOnDemandReferrerSyncFlags(t *testing.T) {
|
||||
Convey("SyncLegacyCosignTags=false syncs OCI referrers but not digest-tag referrers", t, func() {
|
||||
sctlr, srcBaseURL, _, _ := makeUpstreamServer(t, false, false)
|
||||
|
||||
scm := test.NewControllerManager(sctlr)
|
||||
scm.StartAndWait(sctlr.Config.HTTP.Port)
|
||||
|
||||
defer scm.StopServer()
|
||||
|
||||
// ── 1. Fetch the subject digest ──────────────────────────────────────
|
||||
resp, err := resty.R().Get(srcBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
subjectDigest := resp.Header().Get("Docker-Content-Digest")
|
||||
subjectSize := len(resp.Body())
|
||||
|
||||
// ── 2. Push an OCI referrer (attached via subject field) ─────────────
|
||||
_ = pushBlob(srcBaseURL, testImage, ispec.DescriptorEmptyJSON.Data)
|
||||
|
||||
ociRef := ispec.Manifest{
|
||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||
Subject: &ispec.Descriptor{
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Digest: godigest.Digest(subjectDigest),
|
||||
Size: int64(subjectSize),
|
||||
},
|
||||
Config: ispec.Descriptor{
|
||||
MediaType: ispec.MediaTypeEmptyJSON,
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
},
|
||||
Layers: []ispec.Descriptor{{
|
||||
MediaType: ispec.MediaTypeEmptyJSON,
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
}},
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
}
|
||||
|
||||
ociRefBlob, err := json.Marshal(ociRef)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetHeader("Content-type", ispec.MediaTypeImageManifest).
|
||||
SetBody(ociRefBlob).
|
||||
Put(srcBaseURL + "/v2/" + testImage + "/manifests/oci.ref")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
// ── 3. Push a legacy cosign digest-tag referrer (sha256-<hex>.sig) ───
|
||||
sigTag := fmt.Sprintf("%s-%s.sig",
|
||||
godigest.Digest(subjectDigest).Algorithm(),
|
||||
godigest.Digest(subjectDigest).Encoded())
|
||||
|
||||
_ = pushBlob(srcBaseURL, testImage, ispec.DescriptorEmptyJSON.Data)
|
||||
|
||||
sigManifest := ispec.Manifest{
|
||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||
Config: ispec.Descriptor{
|
||||
MediaType: "application/vnd.dev.cosign.simplesigning.v1+json",
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
},
|
||||
Layers: []ispec.Descriptor{{
|
||||
MediaType: ispec.MediaTypeEmptyJSON,
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
}},
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
}
|
||||
|
||||
sigBlob, err := json.Marshal(sigManifest)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetHeader("Content-type", ispec.MediaTypeImageManifest).
|
||||
SetBody(sigBlob).
|
||||
Put(srcBaseURL + "/v2/" + testImage + "/manifests/" + sigTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
// Verify upstream has both the OCI referrer and the sig tag.
|
||||
resp, err = resty.R().Get(srcBaseURL + "/v2/" + testImage + "/referrers/" + subjectDigest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
var upstreamIdx ispec.Index
|
||||
|
||||
So(json.Unmarshal(resp.Body(), &upstreamIdx), ShouldBeNil)
|
||||
So(len(upstreamIdx.Manifests), ShouldEqual, 1) // only OCI referrer in referrers API
|
||||
|
||||
resp, err = resty.R().Get(srcBaseURL + "/v2/" + testImage + "/tags/list")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
So(resp.String(), ShouldContainSubstring, sigTag)
|
||||
|
||||
// ── 4. Start downstream with SyncLegacyCosignTags=false ──────────────
|
||||
var tlsVerify bool
|
||||
syncLegacyFalse := false
|
||||
|
||||
syncRegistryConfig := syncconf.RegistryConfig{
|
||||
Content: []syncconf.Content{{
|
||||
Prefix: testImage,
|
||||
}},
|
||||
URLs: []string{srcBaseURL},
|
||||
TLSVerify: &tlsVerify,
|
||||
OnDemand: true,
|
||||
SyncLegacyCosignTags: &syncLegacyFalse,
|
||||
}
|
||||
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
|
||||
dctlr, destBaseURL, _, _ := makeDownstreamServer(t, false, syncConfig)
|
||||
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
|
||||
defer dcm.StopServer()
|
||||
|
||||
// ── 5. Trigger on-demand sync of the base image ──────────────────────
|
||||
resp, err = resty.R().Get(destBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// ── 6. Trigger on-demand sync of referrers ───────────────────────────
|
||||
resp, err = resty.R().Get(destBaseURL + "/v2/" + testImage + "/referrers/" + subjectDigest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
var downstreamIdx ispec.Index
|
||||
|
||||
So(json.Unmarshal(resp.Body(), &downstreamIdx), ShouldBeNil)
|
||||
// OCI referrer must be present.
|
||||
So(len(downstreamIdx.Manifests), ShouldEqual, 1)
|
||||
|
||||
// ── 7. The sig tag must NOT have been synced ──────────────────────────
|
||||
resp, err = resty.R().Get(destBaseURL + "/v2/" + testImage + "/tags/list")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
So(resp.String(), ShouldNotContainSubstring, sigTag)
|
||||
})
|
||||
|
||||
Convey("On-demand SyncReferrers syncs only direct referrers (no recursion)", t, func() {
|
||||
sctlr, srcBaseURL, _, _ := makeUpstreamServer(t, false, false)
|
||||
|
||||
scm := test.NewControllerManager(sctlr)
|
||||
scm.StartAndWait(sctlr.Config.HTTP.Port)
|
||||
|
||||
defer scm.StopServer()
|
||||
|
||||
// ── 1. Fetch the subject digest ──────────────────────────────────────
|
||||
resp, err := resty.R().Get(srcBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
subjectDigest := resp.Header().Get("Docker-Content-Digest")
|
||||
subjectSize := len(resp.Body())
|
||||
|
||||
// ── 2. Push a direct OCI referrer ────────────────────────────────────
|
||||
_ = pushBlob(srcBaseURL, testImage, ispec.DescriptorEmptyJSON.Data)
|
||||
|
||||
directRef := ispec.Manifest{
|
||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||
Subject: &ispec.Descriptor{
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Digest: godigest.Digest(subjectDigest),
|
||||
Size: int64(subjectSize),
|
||||
},
|
||||
Config: ispec.Descriptor{
|
||||
MediaType: ispec.MediaTypeEmptyJSON,
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
},
|
||||
Layers: []ispec.Descriptor{{
|
||||
MediaType: ispec.MediaTypeEmptyJSON,
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
}},
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Annotations: map[string]string{"level": "direct"},
|
||||
}
|
||||
|
||||
directRefBlob, err := json.Marshal(directRef)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetHeader("Content-type", ispec.MediaTypeImageManifest).
|
||||
SetBody(directRefBlob).
|
||||
Put(srcBaseURL + "/v2/" + testImage + "/manifests/direct.ref")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
directRefDigest := resp.Header().Get("Docker-Content-Digest")
|
||||
|
||||
// ── 3. Push a nested referrer (referrer-of-referrer) ─────────────────
|
||||
nestedRef := ispec.Manifest{
|
||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||
Subject: &ispec.Descriptor{
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Digest: godigest.Digest(directRefDigest),
|
||||
Size: int64(len(directRefBlob)),
|
||||
},
|
||||
Config: ispec.Descriptor{
|
||||
MediaType: ispec.MediaTypeEmptyJSON,
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
},
|
||||
Layers: []ispec.Descriptor{{
|
||||
MediaType: ispec.MediaTypeEmptyJSON,
|
||||
Digest: ispec.DescriptorEmptyJSON.Digest,
|
||||
Size: 2,
|
||||
}},
|
||||
MediaType: ispec.MediaTypeImageManifest,
|
||||
Annotations: map[string]string{"level": "nested"},
|
||||
}
|
||||
|
||||
nestedRefBlob, err := json.Marshal(nestedRef)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err = resty.R().
|
||||
SetHeader("Content-type", ispec.MediaTypeImageManifest).
|
||||
SetBody(nestedRefBlob).
|
||||
Put(srcBaseURL + "/v2/" + testImage + "/manifests/nested.ref")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
||||
|
||||
_ = resp.Header().Get("Docker-Content-Digest")
|
||||
|
||||
// Verify upstream has the referrer chain in place.
|
||||
resp, err = resty.R().Get(srcBaseURL + "/v2/" + testImage + "/referrers/" + subjectDigest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
var upstreamDirect ispec.Index
|
||||
|
||||
So(json.Unmarshal(resp.Body(), &upstreamDirect), ShouldBeNil)
|
||||
So(len(upstreamDirect.Manifests), ShouldEqual, 1)
|
||||
|
||||
resp, err = resty.R().Get(srcBaseURL + "/v2/" + testImage + "/referrers/" + directRefDigest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
var upstreamNested ispec.Index
|
||||
|
||||
So(json.Unmarshal(resp.Body(), &upstreamNested), ShouldBeNil)
|
||||
So(len(upstreamNested.Manifests), ShouldEqual, 1)
|
||||
|
||||
// ── 4. Start downstream with on-demand (referrer sync never recurses) ─
|
||||
var tlsVerify bool
|
||||
syncLegacyFalse := false
|
||||
|
||||
syncRegistryConfig := syncconf.RegistryConfig{
|
||||
Content: []syncconf.Content{{
|
||||
Prefix: testImage,
|
||||
}},
|
||||
URLs: []string{srcBaseURL},
|
||||
TLSVerify: &tlsVerify,
|
||||
OnDemand: true,
|
||||
SyncLegacyCosignTags: &syncLegacyFalse, // avoid legacy tag listing
|
||||
}
|
||||
|
||||
defaultVal := true
|
||||
syncConfig := &syncconf.Config{
|
||||
Enable: &defaultVal,
|
||||
Registries: []syncconf.RegistryConfig{syncRegistryConfig},
|
||||
}
|
||||
|
||||
dctlr, destBaseURL, _, _ := makeDownstreamServer(t, false, syncConfig)
|
||||
|
||||
dcm := test.NewControllerManager(dctlr)
|
||||
dcm.StartAndWait(dctlr.Config.HTTP.Port)
|
||||
|
||||
defer dcm.StopServer()
|
||||
|
||||
// ── 5. Trigger on-demand sync of the base image ──────────────────────
|
||||
resp, err = resty.R().Get(destBaseURL + "/v2/" + testImage + "/manifests/" + testImageTag)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// ── 6. Trigger on-demand sync of referrers for the base image ────────
|
||||
resp, err = resty.R().Get(destBaseURL + "/v2/" + testImage + "/referrers/" + subjectDigest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
var downstreamDirect ispec.Index
|
||||
|
||||
So(json.Unmarshal(resp.Body(), &downstreamDirect), ShouldBeNil)
|
||||
// The direct referrer must be synced.
|
||||
So(len(downstreamDirect.Manifests), ShouldEqual, 1)
|
||||
|
||||
// ── 7. Verify the nested referrer is NOT synced ──────────────────────
|
||||
// On-demand SyncReferrers never recurses; only direct referrers of the
|
||||
// requested subject are synced. So syncing referrers for the base image
|
||||
// did not recursively sync referrers of the direct referrer.
|
||||
//
|
||||
// We MUST NOT query the Referrers API on the downstream here, because
|
||||
// that would trigger a second on-demand sync for the direct referrer.
|
||||
// Instead, we check the metaDB to see if any referrers are recorded for
|
||||
// the directRefDigest.
|
||||
refs, err := dctlr.MetaDB.GetReferrersInfo(testImage, godigest.Digest(directRefDigest), nil)
|
||||
So(err, ShouldBeNil)
|
||||
// If recursive sync had happened, this would be 1.
|
||||
So(len(refs), ShouldEqual, 0)
|
||||
|
||||
// ── 8. Verify that nested referrer is synced when explicitly requested
|
||||
resp, err = resty.R().Get(destBaseURL + "/v2/" + testImage + "/referrers/" + directRefDigest)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
var downstreamNested ispec.Index
|
||||
|
||||
So(json.Unmarshal(resp.Body(), &downstreamNested), ShouldBeNil)
|
||||
So(len(downstreamNested.Manifests), ShouldEqual, 1)
|
||||
refs, err = dctlr.MetaDB.GetReferrersInfo(testImage, godigest.Digest(directRefDigest), nil)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(refs), ShouldEqual, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func generateKeyPairs(tdir string) {
|
||||
// generate a keypair
|
||||
os.Setenv("COSIGN_PASSWORD", "")
|
||||
|
||||
Reference in New Issue
Block a user