Skip to content

Commit e8a3e0e

Browse files
committed
feat: support pushing multiple tags for a single manifest
See opencontainers/distribution-spec#600 Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
1 parent 9c7e77e commit e8a3e0e

21 files changed

Lines changed: 456 additions & 211 deletions

File tree

errors/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ var (
117117
ErrEmptyRepoName = errors.New("repo name can't be empty string")
118118
ErrEmptyTag = errors.New("tag can't be empty string")
119119
ErrEmptyDigest = errors.New("digest can't be empty string")
120+
ErrEmptyManifestTagQuery = errors.New("empty tag query parameter")
120121
ErrInvalidRepoRefFormat = errors.New("invalid image reference format, use [repo:tag] or [repo@digest]")
121122
ErrLimitIsNegative = errors.New("pagination limit has negative value")
122123
ErrLimitIsExcessive = errors.New("pagination limit has excessive value")

pkg/api/constants/consts.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ package constants
33
import "time"
44

55
const (
6-
RoutePrefix = "/v2"
7-
Blobs = "blobs"
8-
Uploads = "uploads"
9-
DistAPIVersion = "Docker-Distribution-API-Version"
10-
DistContentDigestKey = "Docker-Content-Digest"
11-
SubjectDigestKey = "OCI-Subject"
6+
RoutePrefix = "/v2"
7+
Blobs = "blobs"
8+
Uploads = "uploads"
9+
DistAPIVersion = "Docker-Distribution-API-Version"
10+
DistContentDigestKey = "Docker-Content-Digest"
11+
// OCITagResponseKey is returned on digest manifest pushes that include tag query
12+
// parameters (distribution-spec PR #600).
13+
OCITagResponseKey = "OCI-Tag"
14+
SubjectDigestKey = "OCI-Subject"
15+
// MaxManifestDigestQueryTags is the maximum number of tag query parameters accepted on
16+
// PUT .../manifests/<digest>?tag=... (draft OCI distribution-spec: registries MUST
17+
// support at least this many and MAY respond with 414 beyond it).
18+
MaxManifestDigestQueryTags = 10
1219
BlobUploadUUID = "Blob-Upload-UUID"
1320
DefaultMediaType = "application/json"
1421
BinaryMediaType = "application/octet-stream"

pkg/api/routes.go

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,34 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
727727
return
728728
}
729729

730-
digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body)
730+
rawTagQuery, tagQueryPresent := request.URL.Query()["tag"]
731+
digestQueryTags, normErr := normalizeManifestExtraTags(tagQueryPresent, rawTagQuery)
732+
if normErr != nil {
733+
err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"reason": normErr.Error()})
734+
zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(err))
735+
736+
return
737+
}
738+
739+
if len(digestQueryTags) > 0 && !zcommon.IsDigest(reference) {
740+
err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{
741+
"reason": "tag query parameters are only valid when pushing a manifest by digest",
742+
})
743+
zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(err))
744+
745+
return
746+
}
747+
748+
if len(digestQueryTags) > constants.MaxManifestDigestQueryTags {
749+
e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{
750+
"reason": fmt.Sprintf("too many tag query parameters (max %d)", constants.MaxManifestDigestQueryTags),
751+
})
752+
zcommon.WriteJSON(response, http.StatusRequestURITooLong, apiErr.NewErrorList(e))
753+
754+
return
755+
}
756+
757+
digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body, digestQueryTags)
731758
if err != nil {
732759
details := zerr.GetDetails(err)
733760
if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
@@ -769,12 +796,33 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
769796
}
770797

771798
if rh.c.MetaDB != nil {
772-
err := meta.OnUpdateManifest(request.Context(), name, reference, mediaType,
773-
digest, body, rh.c.StoreController, rh.c.MetaDB, rh.c.Log)
774-
if err != nil {
775-
response.WriteHeader(http.StatusInternalServerError)
799+
if len(digestQueryTags) > 0 {
800+
metaUpdateFailed := false
776801

777-
return
802+
for _, tag := range digestQueryTags {
803+
mErr := meta.SetImageMetaFromInput(request.Context(), name, tag, mediaType,
804+
digest, body, imgStore, rh.c.MetaDB, rh.c.Log)
805+
if mErr != nil {
806+
rh.c.Log.Error().Err(mErr).Str("repository", name).Str("tag", tag).
807+
Msg("multi-tag digest push: failed to update meta for tag")
808+
809+
metaUpdateFailed = true
810+
}
811+
}
812+
813+
if metaUpdateFailed {
814+
response.WriteHeader(http.StatusInternalServerError)
815+
816+
return
817+
}
818+
} else {
819+
err := meta.OnUpdateManifest(request.Context(), name, reference, mediaType,
820+
digest, body, rh.c.StoreController, rh.c.MetaDB, rh.c.Log)
821+
if err != nil {
822+
response.WriteHeader(http.StatusInternalServerError)
823+
824+
return
825+
}
778826
}
779827
}
780828

@@ -784,9 +832,45 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
784832

785833
response.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest))
786834
response.Header().Set(constants.DistContentDigestKey, digest.String())
835+
836+
for _, tag := range digestQueryTags {
837+
response.Header().Add(constants.OCITagResponseKey, tag) //nolint:canonicalheader
838+
}
839+
787840
response.WriteHeader(http.StatusCreated)
788841
}
789842

843+
// normalizeManifestExtraTags deduplicates tag query values in order and rejects empty tag components.
844+
func normalizeManifestExtraTags(tagQueryPresent bool, raw []string) ([]string, error) {
845+
if !tagQueryPresent {
846+
return nil, nil
847+
}
848+
849+
seen := map[string]struct{}{}
850+
851+
out := make([]string, 0, len(raw))
852+
853+
for _, rawTag := range raw {
854+
rawTag = strings.TrimSpace(rawTag)
855+
if rawTag == "" {
856+
return nil, zerr.ErrEmptyManifestTagQuery
857+
}
858+
859+
if _, ok := seen[rawTag]; ok {
860+
continue
861+
}
862+
863+
seen[rawTag] = struct{}{}
864+
out = append(out, rawTag)
865+
}
866+
867+
if len(out) == 0 {
868+
return nil, zerr.ErrEmptyManifestTagQuery
869+
}
870+
871+
return out, nil
872+
}
873+
790874
// DeleteManifest godoc
791875
// @Summary Delete image manifest
792876
// @Description Delete an image's manifest given a reference or a digest

pkg/api/routes_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ func TestRoutes(t *testing.T) {
265265
"reference": "reference",
266266
},
267267
&mocks.MockedImageStore{
268-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
268+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
269269
godigest.Digest, error,
270270
) {
271271
return "", "", zerr.ErrRepoNotFound
@@ -280,7 +280,7 @@ func TestRoutes(t *testing.T) {
280280
},
281281

282282
&mocks.MockedImageStore{
283-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
283+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
284284
godigest.Digest, error,
285285
) {
286286
return "", "", zerr.ErrManifestNotFound
@@ -294,7 +294,7 @@ func TestRoutes(t *testing.T) {
294294
"reference": "reference",
295295
},
296296
&mocks.MockedImageStore{
297-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
297+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
298298
godigest.Digest, error,
299299
) {
300300
return "", "", zerr.ErrBadManifest
@@ -308,7 +308,7 @@ func TestRoutes(t *testing.T) {
308308
"reference": "reference",
309309
},
310310
&mocks.MockedImageStore{
311-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
311+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
312312
godigest.Digest, error,
313313
) {
314314
return "", "", zerr.ErrBlobNotFound
@@ -323,7 +323,7 @@ func TestRoutes(t *testing.T) {
323323
"reference": "reference",
324324
},
325325
&mocks.MockedImageStore{
326-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
326+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
327327
godigest.Digest, error,
328328
) {
329329
return "", "", zerr.ErrRepoBadVersion

pkg/extensions/search/search_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4271,7 +4271,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo
42714271
So(err, ShouldBeNil)
42724272

42734273
indexMultiArchMiddle1Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName,
4274-
"multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob)
4274+
"multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob, nil)
42754275
So(err, ShouldBeNil)
42764276

42774277
image211 := CreateRandomImage()
@@ -4296,7 +4296,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo
42964296
So(err, ShouldBeNil)
42974297

42984298
indexMultiArchMiddle2Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName,
4299-
"multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob)
4299+
"multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob, nil)
43004300
So(err, ShouldBeNil)
43014301

43024302
image31 := CreateRandomImage()
@@ -4331,7 +4331,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo
43314331
So(err, ShouldBeNil)
43324332

43334333
_, _, err = storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, "multiArchTop", ispec.MediaTypeImageIndex,
4334-
indexMultiArchTopBlob)
4334+
indexMultiArchTopBlob, nil)
43354335
So(err, ShouldBeNil)
43364336

43374337
ctlrManager.StartAndWait(port)
@@ -4443,7 +4443,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo
44434443
So(err, ShouldBeNil)
44444444

44454445
indexMultiArchMiddle1Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName,
4446-
"multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob)
4446+
"multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob, nil)
44474447
So(err, ShouldBeNil)
44484448

44494449
image211 := CreateRandomImage()
@@ -4468,7 +4468,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo
44684468
So(err, ShouldBeNil)
44694469

44704470
indexMultiArchMiddle2Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName,
4471-
"multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob)
4471+
"multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob, nil)
44724472
So(err, ShouldBeNil)
44734473

44744474
image31 := CreateRandomImage()
@@ -4503,7 +4503,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo
45034503
So(err, ShouldBeNil)
45044504

45054505
_, _, err = storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, "multiArchTop", ispec.MediaTypeImageIndex,
4506-
indexMultiArchTopBlob)
4506+
indexMultiArchTopBlob, nil)
45074507
So(err, ShouldBeNil)
45084508

45094509
ctlr := api.NewController(conf)
@@ -5229,7 +5229,7 @@ func TestMetaDBWhenSigningImages(t *testing.T) {
52295229
Convey("imageIsSignature fails", func() {
52305230
// make image store ignore the wrong format of the input
52315231
ctlr.StoreController.DefaultStore = mocks.MockedImageStore{
5232-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
5232+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
52335233
godigest.Digest, error,
52345234
) {
52355235
return "", "", nil
@@ -6626,7 +6626,7 @@ func TestMetaDBWhenDeletingImages(t *testing.T) {
66266626

66276627
Convey("imageIsSignature fails", func() {
66286628
ctlr.StoreController.DefaultStore = mocks.MockedImageStore{
6629-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
6629+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
66306630
godigest.Digest, error,
66316631
) {
66326632
return "", "", nil
@@ -6652,7 +6652,7 @@ func TestMetaDBWhenDeletingImages(t *testing.T) {
66526652

66536653
return configBlob, nil
66546654
},
6655-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
6655+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
66566656
godigest.Digest, error,
66576657
) {
66586658
return "", "", nil
@@ -6682,7 +6682,7 @@ func TestMetaDBWhenDeletingImages(t *testing.T) {
66826682

66836683
return configBlob, nil
66846684
},
6685-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest,
6685+
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
66866686
godigest.Digest, error,
66876687
) {
66886688
return "", "", ErrTestError

pkg/extensions/sync/destination.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func (registry *DestinationRegistry) copyManifest(repo string, desc ispec.Descri
218218
}
219219

220220
digest, _, err := imageStore.PutImageManifest(repo, reference,
221-
desc.MediaType, manifestContent)
221+
desc.MediaType, manifestContent, nil)
222222
if err != nil {
223223
registry.log.Error().Str("errorType", common.TypeOf(err)).
224224
Err(err).Msg("couldn't upload manifest")
@@ -299,7 +299,7 @@ func (registry *DestinationRegistry) copyManifest(repo string, desc ispec.Descri
299299
return firstMissingErr
300300
}
301301

302-
_, _, err := imageStore.PutImageManifest(repo, reference, desc.MediaType, manifestContent)
302+
_, _, err := imageStore.PutImageManifest(repo, reference, desc.MediaType, manifestContent, nil)
303303
if err != nil {
304304
registry.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).Str("reference", reference).
305305
Err(err).Msg("failed to upload manifest")

pkg/extensions/sync/sync_internal_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@ func TestDestinationRegistry(t *testing.T) {
864864
So(err, ShouldBeNil)
865865
digest = godigest.FromBytes(content)
866866
So(digest, ShouldNotBeNil)
867-
_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content)
867+
_, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil)
868868
So(err, ShouldBeNil)
869869

870870
index.Manifests = append(index.Manifests, ispec.Descriptor{
@@ -880,7 +880,7 @@ func TestDestinationRegistry(t *testing.T) {
880880
indexDigest := godigest.FromBytes(indexContent)
881881
So(indexDigest, ShouldNotBeNil)
882882

883-
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent)
883+
_, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil)
884884
So(err, ShouldBeNil)
885885

886886
Convey("sync index image", func() {
@@ -1067,7 +1067,7 @@ func TestDestinationRegistry(t *testing.T) {
10671067
So(manifestDigest, ShouldNotBeNil)
10681068

10691069
// Store the manifest in the temp image store
1070-
_, _, err = tempImgStore.PutImageManifest(repoName, manifestDigest.String(), ispec.MediaTypeImageManifest, manifestContent)
1070+
_, _, err = tempImgStore.PutImageManifest(repoName, manifestDigest.String(), ispec.MediaTypeImageManifest, manifestContent, nil)
10711071
So(err, ShouldBeNil)
10721072

10731073
// Add to index
@@ -1085,7 +1085,7 @@ func TestDestinationRegistry(t *testing.T) {
10851085
So(indexDigest, ShouldNotBeNil)
10861086

10871087
// Store the index manifest in the temp image store
1088-
_, _, err = tempImgStore.PutImageManifest(repoName, indexDigest.String(), ispec.MediaTypeImageIndex, indexContent)
1088+
_, _, err = tempImgStore.PutImageManifest(repoName, indexDigest.String(), ispec.MediaTypeImageIndex, indexContent, nil)
10891089
So(err, ShouldBeNil)
10901090

10911091
// Now remove one of the child manifest blobs to trigger the error
@@ -1178,7 +1178,7 @@ func TestDestinationRegistry(t *testing.T) {
11781178
digest = godigest.FromBytes(content)
11791179
So(digest, ShouldNotBeNil)
11801180

1181-
_, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, content)
1181+
_, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, content, nil)
11821182
So(err, ShouldBeNil)
11831183

11841184
Convey("sync image", func() {

pkg/meta/hooks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func OnDeleteManifest(repo, reference, mediaType string, digest godigest.Digest,
8282
log.Info().Str("component", "metadb").Msg("restoring image store")
8383

8484
// restore image store
85-
_, _, err := imgStore.PutImageManifest(repo, reference, mediaType, manifestBlob)
85+
_, _, err := imgStore.PutImageManifest(repo, reference, mediaType, manifestBlob, nil)
8686
if err != nil {
8787
log.Error().Err(err).Str("component", "metadb").
8888
Msg("failed to restore manifest to image store, database is not consistent")

0 commit comments

Comments
 (0)