mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
test: add tests for pushing manifests with non-canonical digests together with tags (#3920)
test: add tests for pushing manifests with non-cannonical digests together with tags Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
This commit is contained in:
+96
-116
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
@@ -16,14 +17,75 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPostBlob = errors.New("can't post blob")
|
||||
ErrPutBlob = errors.New("can't put blob")
|
||||
ErrPutIndex = errors.New("can't put index")
|
||||
ErrPostBlob = errors.New("can't post blob")
|
||||
ErrPutBlob = errors.New("can't put blob")
|
||||
ErrPutIndex = errors.New("can't put index")
|
||||
ErrInvalidRefForExtraTags = errors.New("ref must be empty or a valid digest when using extra tags")
|
||||
)
|
||||
|
||||
// UploadOption configures an upload request.
|
||||
type UploadOption func(*uploadConfig)
|
||||
|
||||
type uploadConfig struct {
|
||||
user string
|
||||
password string
|
||||
extraTags []string
|
||||
}
|
||||
|
||||
func (c *uploadConfig) withAuth(req *resty.Request) *resty.Request {
|
||||
if c.user != "" {
|
||||
return req.SetBasicAuth(c.user, c.password)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func (c *uploadConfig) withTagParams(req *resty.Request) *resty.Request {
|
||||
tagParams := make(url.Values)
|
||||
|
||||
for _, t := range c.extraTags {
|
||||
tagParams.Add("tag", t)
|
||||
}
|
||||
|
||||
return req.SetMultiValueQueryParams(tagParams)
|
||||
}
|
||||
|
||||
// WithBasicAuth sets HTTP basic authentication credentials for the upload.
|
||||
func WithBasicAuth(user, password string) UploadOption {
|
||||
return func(c *uploadConfig) {
|
||||
c.user = user
|
||||
c.password = password
|
||||
}
|
||||
}
|
||||
|
||||
// WithExtraTags attaches additional tags to the manifest via the digest-push
|
||||
// API (PUT /v2/{repo}/manifests/{digest}?tag=...).
|
||||
func WithExtraTags(tags ...string) UploadOption {
|
||||
return func(c *uploadConfig) {
|
||||
c.extraTags = append(c.extraTags, tags...)
|
||||
}
|
||||
}
|
||||
|
||||
func UploadImage(img Image, baseURL, repo, ref string) error {
|
||||
return UploadImageWithOpts(img, baseURL, repo, ref)
|
||||
}
|
||||
|
||||
func UploadImageWithBasicAuth(img Image, baseURL, repo, ref, user, password string) error {
|
||||
return UploadImageWithOpts(img, baseURL, repo, ref, WithBasicAuth(user, password))
|
||||
}
|
||||
|
||||
func UploadImageWithOpts(img Image, baseURL, repo, ref string, opts ...UploadOption) error {
|
||||
cfg := &uploadConfig{}
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
if ref == "" {
|
||||
ref = img.DigestStr()
|
||||
} else if len(cfg.extraTags) > 0 {
|
||||
if _, err := godigest.Parse(ref); err != nil {
|
||||
return ErrInvalidRefForExtraTags
|
||||
}
|
||||
}
|
||||
|
||||
digestAlgorithm := img.digestAlgorithm
|
||||
@@ -33,7 +95,8 @@ func UploadImage(img Image, baseURL, repo, ref string) error {
|
||||
}
|
||||
|
||||
for _, blob := range img.Layers {
|
||||
resp, err := resty.R().Post(baseURL + "/v2/" + repo + "/blobs/uploads/")
|
||||
resp, err := cfg.withAuth(resty.R()).
|
||||
Post(baseURL + "/v2/" + repo + "/blobs/uploads/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -46,7 +109,7 @@ func UploadImage(img Image, baseURL, repo, ref string) error {
|
||||
|
||||
digest := digestAlgorithm.FromBytes(blob).String()
|
||||
|
||||
resp, err = resty.R().
|
||||
resp, err = cfg.withAuth(resty.R()).
|
||||
SetHeader("Content-Length", strconv.Itoa(len(blob))).
|
||||
SetHeader("Content-Type", "application/octet-stream").
|
||||
SetQueryParam("digest", digest).
|
||||
@@ -81,7 +144,7 @@ func UploadImage(img Image, baseURL, repo, ref string) error {
|
||||
cdigest = ispec.DescriptorEmptyJSON.Digest
|
||||
}
|
||||
|
||||
resp, err := resty.R().
|
||||
resp, err := cfg.withAuth(resty.R()).
|
||||
Post(baseURL + "/v2/" + repo + "/blobs/uploads/")
|
||||
if err = inject.Error(err); err != nil {
|
||||
return err
|
||||
@@ -94,7 +157,7 @@ func UploadImage(img Image, baseURL, repo, ref string) error {
|
||||
loc := tcommon.Location(baseURL, resp)
|
||||
|
||||
// uploading blob should get 201
|
||||
resp, err = resty.R().
|
||||
resp, err = cfg.withAuth(resty.R()).
|
||||
SetHeader("Content-Length", strconv.Itoa(len(cblob))).
|
||||
SetHeader("Content-Type", "application/octet-stream").
|
||||
SetQueryParam("digest", cdigest.String()).
|
||||
@@ -129,7 +192,7 @@ func UploadImage(img Image, baseURL, repo, ref string) error {
|
||||
mediaType = ispec.MediaTypeImageManifest
|
||||
}
|
||||
|
||||
resp, err = resty.R().
|
||||
resp, err = cfg.withTagParams(cfg.withAuth(resty.R())).
|
||||
SetHeader("Content-type", mediaType).
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/" + repo + "/manifests/" + ref)
|
||||
@@ -141,121 +204,38 @@ func UploadImage(img Image, baseURL, repo, ref string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func UploadImageWithBasicAuth(img Image, baseURL, repo, ref, user, password string) error {
|
||||
digestAlgorithm := img.digestAlgorithm
|
||||
|
||||
if digestAlgorithm == "" {
|
||||
digestAlgorithm = godigest.Canonical
|
||||
}
|
||||
|
||||
for _, blob := range img.Layers {
|
||||
resp, err := resty.R().
|
||||
SetBasicAuth(user, password).
|
||||
Post(baseURL + "/v2/" + repo + "/blobs/uploads/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusAccepted {
|
||||
return ErrPostBlob
|
||||
}
|
||||
|
||||
loc := resp.Header().Get("Location")
|
||||
|
||||
digest := digestAlgorithm.FromBytes(blob).String()
|
||||
|
||||
resp, err = resty.R().
|
||||
SetBasicAuth(user, password).
|
||||
SetHeader("Content-Length", strconv.Itoa(len(blob))).
|
||||
SetHeader("Content-Type", "application/octet-stream").
|
||||
SetQueryParam("digest", digest).
|
||||
SetBody(blob).
|
||||
Put(baseURL + loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusCreated {
|
||||
return ErrPutBlob
|
||||
}
|
||||
}
|
||||
// upload config
|
||||
cblob, err := json.Marshal(img.Config)
|
||||
if err = inject.Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cdigest := digestAlgorithm.FromBytes(cblob)
|
||||
|
||||
if img.Manifest.Config.MediaType == ispec.MediaTypeEmptyJSON {
|
||||
cblob = ispec.DescriptorEmptyJSON.Data
|
||||
cdigest = ispec.DescriptorEmptyJSON.Digest
|
||||
}
|
||||
|
||||
resp, err := resty.R().
|
||||
SetBasicAuth(user, password).
|
||||
Post(baseURL + "/v2/" + repo + "/blobs/uploads/")
|
||||
if err = inject.Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if inject.ErrStatusCode(resp.StatusCode()) != http.StatusAccepted || inject.ErrStatusCode(resp.StatusCode()) == -1 {
|
||||
return ErrPostBlob
|
||||
}
|
||||
|
||||
loc := tcommon.Location(baseURL, resp)
|
||||
|
||||
// uploading blob should get 201
|
||||
resp, err = resty.R().
|
||||
SetBasicAuth(user, password).
|
||||
SetHeader("Content-Length", strconv.Itoa(len(cblob))).
|
||||
SetHeader("Content-Type", "application/octet-stream").
|
||||
SetQueryParam("digest", cdigest.String()).
|
||||
SetBody(cblob).
|
||||
Put(loc)
|
||||
if err = inject.Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if inject.ErrStatusCode(resp.StatusCode()) != http.StatusCreated || inject.ErrStatusCode(resp.StatusCode()) == -1 {
|
||||
return ErrPostBlob
|
||||
}
|
||||
|
||||
// put manifest
|
||||
manifestBlob, err := json.Marshal(img.Manifest)
|
||||
if err = inject.Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use the media type from ManifestDescriptor, or fall back to Manifest.MediaType, or default to OCI
|
||||
mediaType := img.ManifestDescriptor.MediaType
|
||||
|
||||
if mediaType == "" {
|
||||
mediaType = img.Manifest.MediaType
|
||||
}
|
||||
|
||||
if mediaType == "" {
|
||||
mediaType = ispec.MediaTypeImageManifest
|
||||
}
|
||||
|
||||
_, err = resty.R().
|
||||
SetBasicAuth(user, password).
|
||||
SetHeader("Content-type", mediaType).
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/" + repo + "/manifests/" + ref)
|
||||
|
||||
return err
|
||||
func UploadMultiarchImage(multiImage MultiarchImage, baseURL string, repo, ref string) error {
|
||||
return UploadMultiarchImageWithOpts(multiImage, baseURL, repo, ref)
|
||||
}
|
||||
|
||||
func UploadMultiarchImage(multiImage MultiarchImage, baseURL string, repo, ref string) error {
|
||||
func UploadMultiarchImageWithOpts(multiImage MultiarchImage, baseURL string, repo, ref string,
|
||||
opts ...UploadOption,
|
||||
) error {
|
||||
cfg := &uploadConfig{}
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
if ref == "" {
|
||||
ref = multiImage.DigestStr()
|
||||
} else if len(cfg.extraTags) > 0 {
|
||||
if _, err := godigest.Parse(ref); err != nil {
|
||||
return ErrInvalidRefForExtraTags
|
||||
}
|
||||
}
|
||||
|
||||
for _, image := range multiImage.Images {
|
||||
err := UploadImage(image, baseURL, repo, image.DigestStr())
|
||||
var perImageOpts []UploadOption
|
||||
if cfg.user != "" {
|
||||
perImageOpts = append(perImageOpts, WithBasicAuth(cfg.user, cfg.password))
|
||||
}
|
||||
|
||||
err := UploadImageWithOpts(image, baseURL, repo, image.DigestStr(), perImageOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// put manifest
|
||||
indexBlob := multiImage.IndexDescriptor.Data
|
||||
|
||||
if len(indexBlob) == 0 {
|
||||
@@ -278,7 +258,7 @@ func UploadMultiarchImage(multiImage MultiarchImage, baseURL string, repo, ref s
|
||||
mediaType = ispec.MediaTypeImageIndex
|
||||
}
|
||||
|
||||
resp, err := resty.R().
|
||||
resp, err := cfg.withTagParams(cfg.withAuth(resty.R())).
|
||||
SetHeader("Content-type", mediaType).
|
||||
SetBody(indexBlob).
|
||||
Put(baseURL + "/v2/" + repo + "/manifests/" + ref)
|
||||
|
||||
@@ -482,6 +482,117 @@ func TestUploadMultiarchImage(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadImageWithOpts(t *testing.T) {
|
||||
Convey("WithBasicAuth uploads image successfully with valid credentials", t, func() {
|
||||
port := tcommon.GetFreePort()
|
||||
baseURL := tcommon.GetBaseURL(port)
|
||||
|
||||
user := "user"
|
||||
password := "password"
|
||||
testString := tcommon.GetBcryptCredString(user, password)
|
||||
|
||||
htpasswdPath := tcommon.MakeHtpasswdFileFromString(t, testString)
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
conf.HTTP.Auth = &config.AuthConfig{
|
||||
HTPasswd: config.AuthHTPasswd{
|
||||
Path: htpasswdPath,
|
||||
},
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlrManager := tcommon.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
layerBlob := []byte("test")
|
||||
|
||||
img := Image{
|
||||
Layers: [][]byte{layerBlob},
|
||||
Manifest: ispec.Manifest{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: 2,
|
||||
},
|
||||
Layers: []ispec.Descriptor{
|
||||
{
|
||||
Digest: godigest.FromBytes(layerBlob),
|
||||
Size: int64(len(layerBlob)),
|
||||
MediaType: ispec.MediaTypeImageLayerGzip,
|
||||
},
|
||||
},
|
||||
Config: ispec.DescriptorEmptyJSON,
|
||||
},
|
||||
Config: ispec.Image{},
|
||||
}
|
||||
|
||||
err := UploadImageWithOpts(img, baseURL, "test", "mytag", WithBasicAuth(user, password))
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("WithExtraTags exercises tag params path", t, func() {
|
||||
port := tcommon.GetFreePort()
|
||||
baseURL := tcommon.GetBaseURL(port)
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
|
||||
ctlrManager := tcommon.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
layerBlob := []byte("test")
|
||||
|
||||
img := Image{
|
||||
Layers: [][]byte{layerBlob},
|
||||
Manifest: ispec.Manifest{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: 2,
|
||||
},
|
||||
Layers: []ispec.Descriptor{
|
||||
{
|
||||
Digest: godigest.FromBytes(layerBlob),
|
||||
Size: int64(len(layerBlob)),
|
||||
MediaType: ispec.MediaTypeImageLayerGzip,
|
||||
},
|
||||
},
|
||||
Config: ispec.DescriptorEmptyJSON,
|
||||
},
|
||||
Config: ispec.Image{},
|
||||
}
|
||||
|
||||
err := UploadImageWithOpts(img, baseURL, "test", "", WithExtraTags("v1", "v2"))
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("WithExtraTags rejects non-digest non-empty ref", t, func() {
|
||||
err := UploadImageWithOpts(
|
||||
Image{Layers: [][]byte{{1, 2, 3}}},
|
||||
"http://127.0.0.1:0",
|
||||
"repo",
|
||||
"latest",
|
||||
WithExtraTags("v1"),
|
||||
)
|
||||
So(err, ShouldEqual, ErrInvalidRefForExtraTags)
|
||||
})
|
||||
|
||||
Convey("UploadMultiarchImageWithOpts rejects non-digest non-empty ref with extra tags", t, func() {
|
||||
err := UploadMultiarchImageWithOpts(
|
||||
MultiarchImage{},
|
||||
"http://127.0.0.1:0",
|
||||
"repo",
|
||||
"latest",
|
||||
WithExtraTags("v1"),
|
||||
)
|
||||
So(err, ShouldEqual, ErrInvalidRefForExtraTags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInjectUploadImageWithBasicAuth(t *testing.T) {
|
||||
Convey("Inject failures for unreachable lines", t, func() {
|
||||
port := tcommon.GetFreePort()
|
||||
|
||||
Reference in New Issue
Block a user