mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-27 04:35:16 +01:00
Add size middleware to support quota
[Add]: 1, size middleware for quota size 2, count middleware for quota artifact count [Support]: 1, put, patch, mount blob 2, put manifest [Refactor]: 1, Add handle response for middlerware 2, Remove the modifyResponse for registry proxy 3, Use the custom response writer to recored status Signed-off-by: wang yan <wangyan@vmware.com>
This commit is contained in:
parent
4763864dae
commit
1dfc47d24e
@ -52,6 +52,9 @@ func AddArtifactNBlobs(afnbs []*models.ArtifactAndBlob) error {
|
|||||||
successNums, err := o.InsertMulti(total, afnbs)
|
successNums, err := o.InsertMulti(total, afnbs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errInsertMultiple = err
|
errInsertMultiple = err
|
||||||
|
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
|
||||||
|
errInsertMultiple = errors.Wrap(errInsertMultiple, ErrDupRows.Error())
|
||||||
|
}
|
||||||
err := o.Rollback()
|
err := o.Rollback()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("fail to rollback when to insert multiple artifact and blobs, %v", err)
|
log.Errorf("fail to rollback when to insert multiple artifact and blobs, %v", err)
|
||||||
|
@ -2,6 +2,7 @@ package dao
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/astaxie/beego/orm"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"strings"
|
"strings"
|
||||||
@ -48,3 +49,16 @@ func DeleteBlob(digest string) error {
|
|||||||
_, err := o.QueryTable("blob").Filter("digest", digest).Delete()
|
_, err := o.QueryTable("blob").Filter("digest", digest).Delete()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasBlobInProject ...
|
||||||
|
func HasBlobInProject(projectID int64, digest string) (bool, error) {
|
||||||
|
var res []orm.Params
|
||||||
|
num, err := GetOrmer().Raw(`SELECT * FROM artifact af LEFT JOIN artifact_blob afnb ON af.digest = afnb.digest_af WHERE af.project_id = ? and afnb.digest_blob = ? `, projectID, digest).Values(&res)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if num == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
@ -63,3 +63,43 @@ func TestDeleteBlob(t *testing.T) {
|
|||||||
err = DeleteBlob(blob.Digest)
|
err = DeleteBlob(blob.Digest)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHasBlobInProject(t *testing.T) {
|
||||||
|
af := &models.Artifact{
|
||||||
|
PID: 1,
|
||||||
|
Repo: "TestHasBlobInProject",
|
||||||
|
Tag: "latest",
|
||||||
|
Digest: "tttt",
|
||||||
|
Kind: "image",
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
_, err := AddArtifact(af)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
afnb1 := &models.ArtifactAndBlob{
|
||||||
|
DigestAF: "tttt",
|
||||||
|
DigestBlob: "zzza",
|
||||||
|
}
|
||||||
|
afnb2 := &models.ArtifactAndBlob{
|
||||||
|
DigestAF: "tttt",
|
||||||
|
DigestBlob: "zzzb",
|
||||||
|
}
|
||||||
|
afnb3 := &models.ArtifactAndBlob{
|
||||||
|
DigestAF: "tttt",
|
||||||
|
DigestBlob: "zzzc",
|
||||||
|
}
|
||||||
|
|
||||||
|
var afnbs []*models.ArtifactAndBlob
|
||||||
|
afnbs = append(afnbs, afnb1)
|
||||||
|
afnbs = append(afnbs, afnb2)
|
||||||
|
afnbs = append(afnbs, afnb3)
|
||||||
|
|
||||||
|
// add
|
||||||
|
err = AddArtifactNBlobs(afnbs)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
has, err := HasBlobInProject(1, "zzzb")
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.True(t, has)
|
||||||
|
}
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
// Copyright Project Harbor Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package blobquota
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
|
||||||
"github.com/opencontainers/go-digest"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type blobQuotaHandler struct {
|
|
||||||
next http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// New ...
|
|
||||||
func New(next http.Handler) http.Handler {
|
|
||||||
return &blobQuotaHandler{
|
|
||||||
next: next,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP ...
|
|
||||||
func (bqh blobQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
if req.Method == http.MethodPut {
|
|
||||||
match, _ := util.MatchPutBlobURL(req)
|
|
||||||
if match {
|
|
||||||
dgstStr := req.FormValue("digest")
|
|
||||||
if dgstStr == "" {
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", "blob digest missing"), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dgst, err := digest.Parse(dgstStr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", "blob digest parsing failed"), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// ToDo lock digest with redis
|
|
||||||
|
|
||||||
// ToDo read placeholder from config
|
|
||||||
state, err := hmacKey("placeholder").unpackUploadState(req.FormValue("_state"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", "failed to decode state"), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Infof("we need to insert blob data into DB.")
|
|
||||||
log.Infof("blob digest, %v", dgst)
|
|
||||||
log.Infof("blob size, %v", state.Offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
bqh.next.ServeHTTP(rw, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// blobUploadState captures the state serializable state of the blob upload.
|
|
||||||
type blobUploadState struct {
|
|
||||||
// name is the primary repository under which the blob will be linked.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// UUID identifies the upload.
|
|
||||||
UUID string
|
|
||||||
|
|
||||||
// offset contains the current progress of the upload.
|
|
||||||
Offset int64
|
|
||||||
|
|
||||||
// StartedAt is the original start time of the upload.
|
|
||||||
StartedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type hmacKey string
|
|
||||||
|
|
||||||
var errInvalidSecret = errors.New("invalid secret")
|
|
||||||
|
|
||||||
// unpackUploadState unpacks and validates the blob upload state from the
|
|
||||||
// token, using the hmacKey secret.
|
|
||||||
func (secret hmacKey) unpackUploadState(token string) (blobUploadState, error) {
|
|
||||||
var state blobUploadState
|
|
||||||
|
|
||||||
tokenBytes, err := base64.URLEncoding.DecodeString(token)
|
|
||||||
if err != nil {
|
|
||||||
return state, err
|
|
||||||
}
|
|
||||||
mac := hmac.New(sha256.New, []byte(secret))
|
|
||||||
|
|
||||||
if len(tokenBytes) < mac.Size() {
|
|
||||||
return state, errInvalidSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
macBytes := tokenBytes[:mac.Size()]
|
|
||||||
messageBytes := tokenBytes[mac.Size():]
|
|
||||||
|
|
||||||
mac.Write(messageBytes)
|
|
||||||
if !hmac.Equal(mac.Sum(nil), macBytes) {
|
|
||||||
return state, errInvalidSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(messageBytes, &state); err != nil {
|
|
||||||
return state, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return state, nil
|
|
||||||
}
|
|
@ -16,12 +16,12 @@ package middlewares
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/blobquota"
|
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/contenttrust"
|
"github.com/goharbor/harbor/src/core/middlewares/contenttrust"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/countquota"
|
"github.com/goharbor/harbor/src/core/middlewares/countquota"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
|
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
|
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/readonly"
|
"github.com/goharbor/harbor/src/core/middlewares/readonly"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/sizequota"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/url"
|
"github.com/goharbor/harbor/src/core/middlewares/url"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/vulnerable"
|
"github.com/goharbor/harbor/src/core/middlewares/vulnerable"
|
||||||
"github.com/justinas/alice"
|
"github.com/justinas/alice"
|
||||||
@ -65,8 +65,8 @@ func (b *DefaultCreator) geMiddleware(mName string) alice.Constructor {
|
|||||||
LISTREPO: func(next http.Handler) http.Handler { return listrepo.New(next) },
|
LISTREPO: func(next http.Handler) http.Handler { return listrepo.New(next) },
|
||||||
CONTENTTRUST: func(next http.Handler) http.Handler { return contenttrust.New(next) },
|
CONTENTTRUST: func(next http.Handler) http.Handler { return contenttrust.New(next) },
|
||||||
VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) },
|
VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) },
|
||||||
|
SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) },
|
||||||
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
|
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
|
||||||
BLOBQUOTA: func(next http.Handler) http.Handler { return blobquota.New(next) },
|
|
||||||
}
|
}
|
||||||
return middlewares[mName]
|
return middlewares[mName]
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ const (
|
|||||||
LISTREPO = "listrepo"
|
LISTREPO = "listrepo"
|
||||||
CONTENTTRUST = "contenttrust"
|
CONTENTTRUST = "contenttrust"
|
||||||
VULNERABLE = "vulnerable"
|
VULNERABLE = "vulnerable"
|
||||||
|
SIZEQUOTA = "sizequota"
|
||||||
COUNTQUOTA = "countquota"
|
COUNTQUOTA = "countquota"
|
||||||
BLOBQUOTA = "blobquota"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Middlewares with sequential organization
|
// Middlewares with sequential organization
|
||||||
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, BLOBQUOTA, COUNTQUOTA}
|
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, COUNTQUOTA}
|
||||||
|
@ -15,41 +15,14 @@
|
|||||||
package countquota
|
package countquota
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/distribution"
|
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
|
||||||
"github.com/docker/distribution/manifest/schema2"
|
|
||||||
"github.com/garyburd/redigo/redis"
|
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/quota"
|
|
||||||
common_util "github.com/goharbor/harbor/src/common/utils"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
common_redis "github.com/goharbor/harbor/src/common/utils/redis"
|
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
dialConnectionTimeout = 30 * time.Second
|
|
||||||
dialReadTimeout = time.Minute + 10*time.Second
|
|
||||||
dialWriteTimeout = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrRequireQuota ...
|
|
||||||
var ErrRequireQuota = errors.New("cannot get quota on project for request")
|
|
||||||
|
|
||||||
type countQuotaHandler struct {
|
type countQuotaHandler struct {
|
||||||
next http.Handler
|
next http.Handler
|
||||||
mfInfo *util.MfInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New ...
|
// New ...
|
||||||
@ -61,158 +34,32 @@ func New(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// ServeHTTP manifest ...
|
// ServeHTTP manifest ...
|
||||||
func (cqh *countQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (cqh *countQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
match, repository, tag := util.MatchPushManifest(req)
|
countInteceptor := getInteceptor(req)
|
||||||
if match {
|
if countInteceptor == nil {
|
||||||
mfInfo := &util.MfInfo{
|
cqh.next.ServeHTTP(rw, req)
|
||||||
Repository: repository,
|
return
|
||||||
Tag: tag,
|
}
|
||||||
}
|
// handler request
|
||||||
cqh.mfInfo = mfInfo
|
if err := countInteceptor.HandleRequest(req); err != nil {
|
||||||
|
log.Warningf("Error occurred when to handle request in count quota handler: %v", err)
|
||||||
mediaType := req.Header.Get("Content-Type")
|
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in count quota handler: %v", err)),
|
||||||
if mediaType == schema1.MediaTypeManifest ||
|
http.StatusInternalServerError)
|
||||||
mediaType == schema1.MediaTypeSignedManifest ||
|
return
|
||||||
mediaType == schema2.MediaTypeManifest {
|
|
||||||
|
|
||||||
tagLock, err := cqh.tryLockTag()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("Error occurred when to lock tag %s:%s with digest %v", repository, tag, err)
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", fmt.Sprintf("Error occurred when to lock tag %s:%s with digest %v", repository, tag, err)), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cqh.mfInfo.TagLock = tagLock
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
cqh.tryFreeTag()
|
|
||||||
log.Warningf("Error occurred when to copy manifest body %v", err)
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", fmt.Sprintf("Error occurred when to decode manifest body %v", err)), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
|
||||||
|
|
||||||
manifest, desc, err := distribution.UnmarshalManifest(mediaType, data)
|
|
||||||
if err != nil {
|
|
||||||
cqh.tryFreeTag()
|
|
||||||
log.Warningf("Error occurred when to Unmarshal Manifest %v", err)
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", fmt.Sprintf("Error occurred when to Unmarshal Manifest %v", err)), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cqh.mfInfo.Refrerence = manifest.References()
|
|
||||||
cqh.mfInfo.Digest = desc.Digest.String()
|
|
||||||
|
|
||||||
projectID, err := cqh.getProjectID(strings.Split(repository, "/")[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("Error occurred when to get project ID %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cqh.mfInfo.ProjectID = projectID
|
|
||||||
|
|
||||||
imageExist, af, err := cqh.imageExist()
|
|
||||||
if err != nil {
|
|
||||||
cqh.tryFreeTag()
|
|
||||||
log.Warningf("Error occurred when to check Manifest existence by repo and tag name %v", err)
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", fmt.Sprintf("Error occurred when to check Manifest existence %v", err)), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cqh.mfInfo.Exist = imageExist
|
|
||||||
if imageExist {
|
|
||||||
if af.Digest != cqh.mfInfo.Digest {
|
|
||||||
cqh.mfInfo.DigestChanged = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
quotaRes := "a.ResourceList{
|
|
||||||
quota.ResourceCount: 1,
|
|
||||||
}
|
|
||||||
err := cqh.tryRequireQuota(quotaRes)
|
|
||||||
if err != nil {
|
|
||||||
cqh.tryFreeTag()
|
|
||||||
log.Errorf("Cannot get quota for the manifest %v", err)
|
|
||||||
if err == ErrRequireQuota {
|
|
||||||
http.Error(rw, util.MarshalError("StatusNotAcceptable", fmt.Sprintf("Cannot get quota for the manifest %v", err)), http.StatusNotAcceptable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(rw, util.MarshalError("InternalServerError", fmt.Sprintf("Error occurred when to require quota for the manifest %v", err)), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cqh.mfInfo.Quota = quotaRes
|
|
||||||
}
|
|
||||||
|
|
||||||
*req = *(req.WithContext(context.WithValue(req.Context(), util.MFInfokKey, mfInfo)))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cqh.next.ServeHTTP(rw, req)
|
cqh.next.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
// handler response
|
||||||
|
countInteceptor.HandleResponse(*rw.(*util.CustomResponseWriter), req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryLockTag locks tag with redis ...
|
func getInteceptor(req *http.Request) util.RegInterceptor {
|
||||||
func (cqh *countQuotaHandler) tryLockTag() (*common_redis.Mutex, error) {
|
// PUT /v2/<name>/manifests/<reference>
|
||||||
con, err := redis.DialURL(
|
matchPushMF, repository, tag := util.MatchPushManifest(req)
|
||||||
config.GetRedisOfRegURL(),
|
if matchPushMF {
|
||||||
redis.DialConnectTimeout(dialConnectionTimeout),
|
mfInfo := util.MfInfo{}
|
||||||
redis.DialReadTimeout(dialReadTimeout),
|
mfInfo.Repository = repository
|
||||||
redis.DialWriteTimeout(dialWriteTimeout),
|
mfInfo.Tag = tag
|
||||||
)
|
return NewPutManifestInterceptor(&mfInfo)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tagLock := common_redis.New(con, cqh.mfInfo.Repository+":"+cqh.mfInfo.Tag, common_util.GenerateRandomString())
|
|
||||||
success, err := tagLock.Require()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !success {
|
|
||||||
return nil, fmt.Errorf("unable to lock tag: %s ", cqh.mfInfo.Repository+":"+cqh.mfInfo.Tag)
|
|
||||||
}
|
|
||||||
return tagLock, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cqh *countQuotaHandler) tryFreeTag() {
|
|
||||||
_, err := cqh.mfInfo.TagLock.Free()
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("Error to unlock tag: %s, with error: %v ", cqh.mfInfo.Tag, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the existence of a artifact, if exist, the method will return the artifact model
|
|
||||||
func (cqh *countQuotaHandler) imageExist() (exist bool, af *models.Artifact, err error) {
|
|
||||||
artifactQuery := &models.ArtifactQuery{
|
|
||||||
PID: cqh.mfInfo.ProjectID,
|
|
||||||
Repo: cqh.mfInfo.Repository,
|
|
||||||
Tag: cqh.mfInfo.Tag,
|
|
||||||
}
|
|
||||||
afs, err := dao.ListArtifacts(artifactQuery)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error occurred when to get project ID %v", err)
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
if len(afs) > 0 {
|
|
||||||
return true, afs[0], nil
|
|
||||||
}
|
|
||||||
return false, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cqh *countQuotaHandler) tryRequireQuota(quotaRes *quota.ResourceList) error {
|
|
||||||
quotaMgr, err := quota.NewManager("project", strconv.FormatInt(cqh.mfInfo.ProjectID, 10))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error occurred when to new quota manager %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := quotaMgr.AddResources(*quotaRes); err != nil {
|
|
||||||
log.Errorf("Cannot get quota for the manifest %v", err)
|
|
||||||
return ErrRequireQuota
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cqh *countQuotaHandler) getProjectID(name string) (int64, error) {
|
|
||||||
project, err := dao.GetProjectByName(name)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if project != nil {
|
|
||||||
return project.ProjectID, nil
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("project %s is not found", name)
|
|
||||||
}
|
|
||||||
|
211
src/core/middlewares/countquota/putmanifest.go
Normal file
211
src/core/middlewares/countquota/putmanifest.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package countquota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/quota"
|
||||||
|
common_util "github.com/goharbor/harbor/src/common/utils"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
common_redis "github.com/goharbor/harbor/src/common/utils/redis"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PutManifestInterceptor ...
|
||||||
|
type PutManifestInterceptor struct {
|
||||||
|
mfInfo *util.MfInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPutManifestInterceptor ...
|
||||||
|
func NewPutManifestInterceptor(mfInfo *util.MfInfo) *PutManifestInterceptor {
|
||||||
|
return &PutManifestInterceptor{
|
||||||
|
mfInfo: mfInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
// The context has already contain mfinfo as it was put by size quota handler.
|
||||||
|
func (pmi *PutManifestInterceptor) HandleRequest(req *http.Request) error {
|
||||||
|
mfInfo := req.Context().Value(util.MFInfokKey)
|
||||||
|
mf, ok := mfInfo.(*util.MfInfo)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("failed to get manifest infor from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
tagLock, err := tryLockTag(mf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error occurred when to lock tag %s:%s with digest %v", mf.Repository, mf.Tag, err)
|
||||||
|
}
|
||||||
|
mf.TagLock = tagLock
|
||||||
|
|
||||||
|
imageExist, af, err := imageExist(mf)
|
||||||
|
if err != nil {
|
||||||
|
tryFreeTag(mf)
|
||||||
|
return fmt.Errorf("error occurred when to check Manifest existence %v", err)
|
||||||
|
}
|
||||||
|
mf.Exist = imageExist
|
||||||
|
if imageExist {
|
||||||
|
if af.Digest != mf.Digest {
|
||||||
|
mf.DigestChanged = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quotaRes := "a.ResourceList{
|
||||||
|
quota.ResourceCount: 1,
|
||||||
|
}
|
||||||
|
err := util.TryRequireQuota(mf.ProjectID, quotaRes)
|
||||||
|
if err != nil {
|
||||||
|
tryFreeTag(mf)
|
||||||
|
log.Errorf("Cannot get quota for the manifest %v", err)
|
||||||
|
if err == util.ErrRequireQuota {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("error occurred when to require quota for the manifest %v", err)
|
||||||
|
}
|
||||||
|
mf.Quota = quotaRes
|
||||||
|
}
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.MFInfokKey, mf)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleResponse ...
|
||||||
|
func (pmi *PutManifestInterceptor) HandleResponse(rw util.CustomResponseWriter, req *http.Request) {
|
||||||
|
mfInfo := req.Context().Value(util.MFInfokKey)
|
||||||
|
mf, ok := mfInfo.(*util.MfInfo)
|
||||||
|
if !ok {
|
||||||
|
log.Error("failed to convert manifest information context into MfInfo")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_, err := mf.TagLock.Free()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error to unlock in response handler, %v", err)
|
||||||
|
}
|
||||||
|
if err := mf.TagLock.Conn.Close(); err != nil {
|
||||||
|
log.Errorf("Error to close redis connection in response handler, %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 201
|
||||||
|
if rw.Status() == http.StatusCreated {
|
||||||
|
af := &models.Artifact{
|
||||||
|
PID: mf.ProjectID,
|
||||||
|
Repo: mf.Repository,
|
||||||
|
Tag: mf.Tag,
|
||||||
|
Digest: mf.Digest,
|
||||||
|
PushTime: time.Now(),
|
||||||
|
Kind: "Docker-Image",
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert or update
|
||||||
|
if !mf.Exist {
|
||||||
|
_, err := dao.AddArtifact(af)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error to add artifact, %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mf.DigestChanged {
|
||||||
|
err := dao.UpdateArtifactDigest(af)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error to add artifact, %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mf.Exist || mf.DigestChanged {
|
||||||
|
afnbs := []*models.ArtifactAndBlob{}
|
||||||
|
self := &models.ArtifactAndBlob{
|
||||||
|
DigestAF: mf.Digest,
|
||||||
|
DigestBlob: mf.Digest,
|
||||||
|
}
|
||||||
|
afnbs = append(afnbs, self)
|
||||||
|
for _, d := range mf.Refrerence {
|
||||||
|
afnb := &models.ArtifactAndBlob{
|
||||||
|
DigestAF: mf.Digest,
|
||||||
|
DigestBlob: d.Digest.String(),
|
||||||
|
}
|
||||||
|
afnbs = append(afnbs, afnb)
|
||||||
|
}
|
||||||
|
if err := dao.AddArtifactNBlobs(afnbs); err != nil {
|
||||||
|
if strings.Contains(err.Error(), dao.ErrDupRows.Error()) {
|
||||||
|
log.Warning("the artifact and blobs have already in the DB, it maybe an existing image with different tag")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Errorf("Error to add artifact and blobs in proxy response handler, %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if rw.Status() >= 300 || rw.Status() <= 511 {
|
||||||
|
if !mf.Exist {
|
||||||
|
success := util.TryFreeQuota(mf.ProjectID, mf.Quota)
|
||||||
|
if !success {
|
||||||
|
log.Error("error to release resource booked for the manifest")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryLockTag locks tag with redis ...
|
||||||
|
func tryLockTag(mfInfo *util.MfInfo) (*common_redis.Mutex, error) {
|
||||||
|
con, err := util.GetRegRedisCon()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tagLock := common_redis.New(con, "Quota::manifest-lock::"+mfInfo.Repository+":"+mfInfo.Tag, common_util.GenerateRandomString())
|
||||||
|
success, err := tagLock.Require()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
return nil, fmt.Errorf("unable to lock tag: %s ", mfInfo.Repository+":"+mfInfo.Tag)
|
||||||
|
}
|
||||||
|
return tagLock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryFreeTag(mfInfo *util.MfInfo) {
|
||||||
|
_, err := mfInfo.TagLock.Free()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error to unlock tag: %s, with error: %v ", mfInfo.Tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the existence of a artifact, if exist, the method will return the artifact model
|
||||||
|
func imageExist(mfInfo *util.MfInfo) (exist bool, af *models.Artifact, err error) {
|
||||||
|
artifactQuery := &models.ArtifactQuery{
|
||||||
|
PID: mfInfo.ProjectID,
|
||||||
|
Repo: mfInfo.Repository,
|
||||||
|
Tag: mfInfo.Tag,
|
||||||
|
}
|
||||||
|
afs, err := dao.ListArtifacts(artifactQuery)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error occurred when to get project ID %v", err)
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
if len(afs) > 0 {
|
||||||
|
return true, afs[0], nil
|
||||||
|
}
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
@ -17,6 +17,7 @@ package middlewares
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/registryproxy"
|
"github.com/goharbor/harbor/src/core/middlewares/registryproxy"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,5 +36,6 @@ func Init() error {
|
|||||||
|
|
||||||
// Handle handles the request.
|
// Handle handles the request.
|
||||||
func Handle(rw http.ResponseWriter, req *http.Request) {
|
func Handle(rw http.ResponseWriter, req *http.Request) {
|
||||||
head.ServeHTTP(rw, req)
|
customResW := util.NewCustomResponseWriter(rw)
|
||||||
|
head.ServeHTTP(customResW, req)
|
||||||
}
|
}
|
||||||
|
@ -15,19 +15,11 @@
|
|||||||
package registryproxy
|
package registryproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
|
||||||
"github.com/goharbor/harbor/src/common/quota"
|
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type proxyHandler struct {
|
type proxyHandler struct {
|
||||||
@ -58,151 +50,11 @@ func New(urls ...string) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &proxyHandler{
|
return &proxyHandler{
|
||||||
handler: &httputil.ReverseProxy{
|
handler: httputil.NewSingleHostReverseProxy(targetURL),
|
||||||
Director: func(req *http.Request) {
|
|
||||||
director(targetURL, req)
|
|
||||||
},
|
|
||||||
ModifyResponse: modifyResponse,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite the http requests
|
|
||||||
func director(target *url.URL, req *http.Request) {
|
|
||||||
targetQuery := target.RawQuery
|
|
||||||
req.URL.Scheme = target.Scheme
|
|
||||||
req.URL.Host = target.Host
|
|
||||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
|
||||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
|
||||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
|
||||||
} else {
|
|
||||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
|
||||||
}
|
|
||||||
if _, ok := req.Header["User-Agent"]; !ok {
|
|
||||||
// explicitly disable User-Agent so it's not set to default value
|
|
||||||
req.Header.Set("User-Agent", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify the http response
|
|
||||||
func modifyResponse(res *http.Response) error {
|
|
||||||
matchMF, _, _ := util.MatchPushManifest(res.Request)
|
|
||||||
if matchMF {
|
|
||||||
return handlerPutManifest(res)
|
|
||||||
}
|
|
||||||
matchBB, _ := util.MatchPutBlobURL(res.Request)
|
|
||||||
if matchBB {
|
|
||||||
return handlerPutBlob(res)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func singleJoiningSlash(a, b string) string {
|
|
||||||
aslash := strings.HasSuffix(a, "/")
|
|
||||||
bslash := strings.HasPrefix(b, "/")
|
|
||||||
switch {
|
|
||||||
case aslash && bslash:
|
|
||||||
return a + b[1:]
|
|
||||||
case !aslash && !bslash:
|
|
||||||
return a + "/" + b
|
|
||||||
}
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerPutManifest(res *http.Response) error {
|
|
||||||
mfInfo := res.Request.Context().Value(util.MFInfokKey)
|
|
||||||
mf, ok := mfInfo.(*util.MfInfo)
|
|
||||||
if !ok {
|
|
||||||
return errors.New("failed to convert manifest information context into MfInfo")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
_, err := mf.TagLock.Free()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error to unlock in response handler, %v", err)
|
|
||||||
}
|
|
||||||
if err := mf.TagLock.Conn.Close(); err != nil {
|
|
||||||
log.Errorf("Error to close redis connection in response handler, %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 201
|
|
||||||
if res.StatusCode == http.StatusCreated {
|
|
||||||
af := &models.Artifact{
|
|
||||||
PID: mf.ProjectID,
|
|
||||||
Repo: mf.Repository,
|
|
||||||
Tag: mf.Tag,
|
|
||||||
Digest: mf.Digest,
|
|
||||||
PushTime: time.Now(),
|
|
||||||
Kind: "Docker-Image",
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert or update
|
|
||||||
if !mf.Exist {
|
|
||||||
_, err := dao.AddArtifact(af)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error to add artifact, %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if mf.DigestChanged {
|
|
||||||
err := dao.UpdateArtifactDigest(af)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error to add artifact, %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mf.Exist || mf.DigestChanged {
|
|
||||||
afnbs := []*models.ArtifactAndBlob{}
|
|
||||||
for _, d := range mf.Refrerence {
|
|
||||||
afnb := &models.ArtifactAndBlob{
|
|
||||||
DigestAF: mf.Digest,
|
|
||||||
DigestBlob: d.Digest.String(),
|
|
||||||
}
|
|
||||||
afnbs = append(afnbs, afnb)
|
|
||||||
}
|
|
||||||
if err := dao.AddArtifactNBlobs(afnbs); err != nil {
|
|
||||||
log.Errorf("Error to add artifact and blobs in proxy response handler, %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if res.StatusCode >= 300 || res.StatusCode <= 511 {
|
|
||||||
if !mf.Exist {
|
|
||||||
success := subtractResources(mf)
|
|
||||||
if !success {
|
|
||||||
return errors.New("Error to release resource booked for the manifest")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerPutBlob(res *http.Response) error {
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
|
||||||
log.Infof("we need to rollback DB and unlock digest ... ")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// used to release resource for failure case
|
|
||||||
func subtractResources(mfInfo *util.MfInfo) bool {
|
|
||||||
quotaMgr, err := quota.NewManager("project", strconv.FormatInt(mfInfo.ProjectID, 10))
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error occurred when to new quota manager %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := quotaMgr.SubtractResources(*mfInfo.Quota); err != nil {
|
|
||||||
log.Errorf("Cannot get quota for the manifest %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP ...
|
// ServeHTTP ...
|
||||||
func (ph proxyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (ph proxyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
ph.handler.ServeHTTP(rw, req)
|
ph.handler.ServeHTTP(rw, req)
|
||||||
|
231
src/core/middlewares/sizequota/handler.go
Normal file
231
src/core/middlewares/sizequota/handler.go
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
|
"github.com/goharbor/harbor/src/common/quota"
|
||||||
|
common_util "github.com/goharbor/harbor/src/common/utils"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
common_redis "github.com/goharbor/harbor/src/common/utils/redis"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sizeQuotaHandler struct {
|
||||||
|
next http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// New ...
|
||||||
|
func New(next http.Handler) http.Handler {
|
||||||
|
return &sizeQuotaHandler{
|
||||||
|
next: next,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP ...
|
||||||
|
func (sqh *sizeQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
sizeInteceptor := getInteceptor(req)
|
||||||
|
if sizeInteceptor == nil {
|
||||||
|
sqh.next.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler request
|
||||||
|
if err := sizeInteceptor.HandleRequest(req); err != nil {
|
||||||
|
log.Warningf("Error occurred when to handle request in size quota handler: %v", err)
|
||||||
|
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in size quota handler: %v", err)),
|
||||||
|
http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sqh.next.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
// handler response
|
||||||
|
sizeInteceptor.HandleResponse(*rw.(*util.CustomResponseWriter), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInteceptor(req *http.Request) util.RegInterceptor {
|
||||||
|
// POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name>
|
||||||
|
matchMountBlob, repository, mount, _ := util.MatchMountBlobURL(req)
|
||||||
|
if matchMountBlob {
|
||||||
|
bb := util.BlobInfo{}
|
||||||
|
bb.Repository = repository
|
||||||
|
bb.Digest = mount
|
||||||
|
return NewMountBlobInterceptor(&bb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
|
||||||
|
matchPutBlob, repository := util.MatchPutBlobURL(req)
|
||||||
|
if matchPutBlob {
|
||||||
|
bb := util.BlobInfo{}
|
||||||
|
bb.Repository = repository
|
||||||
|
return NewPutBlobInterceptor(&bb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /v2/<name>/manifests/<reference>
|
||||||
|
matchPushMF, repository, tag := util.MatchPushManifest(req)
|
||||||
|
if matchPushMF {
|
||||||
|
bb := util.BlobInfo{}
|
||||||
|
mfInfo := util.MfInfo{}
|
||||||
|
bb.Repository = repository
|
||||||
|
mfInfo.Repository = repository
|
||||||
|
mfInfo.Tag = tag
|
||||||
|
return NewPutManifestInterceptor(&bb, &mfInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /v2/<name>/blobs/uploads/<uuid>
|
||||||
|
matchPatchBlob, _ := util.MatchPatchBlobURL(req)
|
||||||
|
if matchPatchBlob {
|
||||||
|
return NewPatchBlobInterceptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireQuota(conn redis.Conn, blobInfo *util.BlobInfo) error {
|
||||||
|
projectID, err := util.GetProjectID(strings.Split(blobInfo.Repository, "/")[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
blobInfo.ProjectID = projectID
|
||||||
|
|
||||||
|
digestLock, err := tryLockBlob(conn, blobInfo)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("failed to lock digest in redis, %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
blobInfo.DigestLock = digestLock
|
||||||
|
|
||||||
|
blobExist, err := dao.HasBlobInProject(blobInfo.ProjectID, blobInfo.Digest)
|
||||||
|
if err != nil {
|
||||||
|
tryFreeBlob(blobInfo)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
blobInfo.Exist = blobExist
|
||||||
|
if blobExist {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// only require quota for non existing blob.
|
||||||
|
quotaRes := "a.ResourceList{
|
||||||
|
quota.ResourceStorage: blobInfo.Size,
|
||||||
|
}
|
||||||
|
err = util.TryRequireQuota(blobInfo.ProjectID, quotaRes)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("project id, %d, size %d", blobInfo.ProjectID, blobInfo.Size)
|
||||||
|
tryFreeBlob(blobInfo)
|
||||||
|
log.Errorf("cannot get quota for the blob %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
blobInfo.Quota = quotaRes
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleBlobCommon handles put blob complete request
|
||||||
|
// 1, add blob into DB if success
|
||||||
|
// 2, roll back resource if failure.
|
||||||
|
func HandleBlobCommon(rw util.CustomResponseWriter, req *http.Request) error {
|
||||||
|
bbInfo := req.Context().Value(util.BBInfokKey)
|
||||||
|
bb, ok := bbInfo.(*util.BlobInfo)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("failed to convert blob information context into BBInfo")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_, err := bb.DigestLock.Free()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error to unlock blob digest:%s in response handler, %v", bb.Digest, err)
|
||||||
|
}
|
||||||
|
if err := bb.DigestLock.Conn.Close(); err != nil {
|
||||||
|
log.Errorf("Error to close redis connection in put blob response handler, %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Do nothing for a existing blob.
|
||||||
|
if bb.Exist {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if rw.Status() == http.StatusCreated {
|
||||||
|
blob := &models.Blob{
|
||||||
|
Digest: bb.Digest,
|
||||||
|
ContentType: bb.ContentType,
|
||||||
|
Size: bb.Size,
|
||||||
|
CreationTime: time.Now(),
|
||||||
|
}
|
||||||
|
_, err := dao.AddBlob(blob)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if rw.Status() >= 300 || rw.Status() <= 511 {
|
||||||
|
success := util.TryFreeQuota(bb.ProjectID, bb.Quota)
|
||||||
|
if !success {
|
||||||
|
return fmt.Errorf("Error to release resource booked for the blob, %d, digest: %s ", bb.ProjectID, bb.Digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryLockBlob locks blob with redis ...
|
||||||
|
func tryLockBlob(conn redis.Conn, blobInfo *util.BlobInfo) (*common_redis.Mutex, error) {
|
||||||
|
// Quota::blob-lock::projectname::digest
|
||||||
|
digestLock := common_redis.New(conn, "Quota::blob-lock::"+strings.Split(blobInfo.Repository, "/")[0]+":"+blobInfo.Digest, common_util.GenerateRandomString())
|
||||||
|
success, err := digestLock.Require()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
return nil, fmt.Errorf("unable to lock digest: %s, %s ", blobInfo.Repository, blobInfo.Digest)
|
||||||
|
}
|
||||||
|
return digestLock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryFreeBlob(blobInfo *util.BlobInfo) {
|
||||||
|
_, err := blobInfo.DigestLock.Free()
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error to unlock digest: %s,%s with error: %v ", blobInfo.Repository, blobInfo.Digest, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rmBlobUploadUUID(conn redis.Conn, UUID string) (bool, error) {
|
||||||
|
exists, err := redis.Int(conn.Do("EXISTS", UUID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if exists == 1 {
|
||||||
|
res, err := redis.Int(conn.Do("DEL", UUID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return res == 1, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// put blob path: /v2/<name>/blobs/uploads/<uuid>
|
||||||
|
func getUUID(path string) string {
|
||||||
|
if !strings.Contains(path, "/") {
|
||||||
|
log.Infof("it's not a valid path string: %s", path)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
strs := strings.Split(path, "/")
|
||||||
|
return strs[len(strs)-1]
|
||||||
|
}
|
177
src/core/middlewares/sizequota/handler_test.go
Normal file
177
src/core/middlewares/sizequota/handler_test.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
utilstest "github.com/goharbor/harbor/src/common/utils/test"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testingRedisHost = "REDIS_HOST"
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
utilstest.InitDatabaseFromEnv()
|
||||||
|
rc := m.Run()
|
||||||
|
if rc != 0 {
|
||||||
|
os.Exit(rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInteceptor(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
req1, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
res1 := getInteceptor(req1)
|
||||||
|
|
||||||
|
_, ok := res1.(*PutManifestInterceptor)
|
||||||
|
assert.True(ok)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/library/ubuntu/TestGetInteceptor/14.04", nil)
|
||||||
|
res2 := getInteceptor(req2)
|
||||||
|
assert.Nil(res2)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequireQuota(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
blobInfo := &util.BlobInfo{
|
||||||
|
Repository: "library/test",
|
||||||
|
Digest: "sha256:abcdf123sdfefeg1246",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = requireQuota(con, blobInfo)
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUUID(t *testing.T) {
|
||||||
|
str1 := "test/1/2/uuid-1"
|
||||||
|
uuid1 := getUUID(str1)
|
||||||
|
assert.Equal(t, uuid1, "uuid-1")
|
||||||
|
|
||||||
|
// not a valid path, just return empty
|
||||||
|
str2 := "test-1-2-uuid-2"
|
||||||
|
uuid2 := getUUID(str2)
|
||||||
|
assert.Equal(t, uuid2, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddRmUUID(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
rmfail, err := rmBlobUploadUUID(con, "test-rm-uuid")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, rmfail)
|
||||||
|
|
||||||
|
success, err := util.SetBunkSize(con, "test-rm-uuid", 1000)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, success)
|
||||||
|
|
||||||
|
rmSuccess, err := rmBlobUploadUUID(con, "test-rm-uuid")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, rmSuccess)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTryFreeLockBlob(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
blobInfo := util.BlobInfo{
|
||||||
|
Repository: "lock/test",
|
||||||
|
Digest: "sha256:abcdf123sdfefeg1246",
|
||||||
|
}
|
||||||
|
|
||||||
|
lock, err := tryLockBlob(con, &blobInfo)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
blobInfo.DigestLock = lock
|
||||||
|
tryFreeBlob(&blobInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlobCommon(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
blobInfo := util.BlobInfo{
|
||||||
|
Repository: "TestBlobCommon/test",
|
||||||
|
Digest: "sha256:abcdf12345678sdfefeg1246",
|
||||||
|
ContentType: "ContentType",
|
||||||
|
Size: 101,
|
||||||
|
Exist: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
customResW := util.CustomResponseWriter{ResponseWriter: rw}
|
||||||
|
customResW.WriteHeader(201)
|
||||||
|
|
||||||
|
lock, err := tryLockBlob(con, &blobInfo)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
blobInfo.DigestLock = lock
|
||||||
|
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.BBInfokKey, &blobInfo)))
|
||||||
|
|
||||||
|
err = HandleBlobCommon(customResW, req)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRedisHost() string {
|
||||||
|
redisHost := os.Getenv(testingRedisHost)
|
||||||
|
if redisHost == "" {
|
||||||
|
redisHost = "127.0.0.1" // for local test
|
||||||
|
}
|
||||||
|
|
||||||
|
return redisHost
|
||||||
|
}
|
69
src/core/middlewares/sizequota/mountblob.go
Normal file
69
src/core/middlewares/sizequota/mountblob.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MountBlobInterceptor ...
|
||||||
|
type MountBlobInterceptor struct {
|
||||||
|
blobInfo *util.BlobInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMountBlobInterceptor ...
|
||||||
|
func NewMountBlobInterceptor(blobInfo *util.BlobInfo) *MountBlobInterceptor {
|
||||||
|
return &MountBlobInterceptor{
|
||||||
|
blobInfo: blobInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
func (mbi *MountBlobInterceptor) HandleRequest(req *http.Request) error {
|
||||||
|
tProjectID, err := util.GetProjectID(strings.Split(mbi.blobInfo.Repository, "/")[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error occurred when to get target project: %d, %v", tProjectID, err)
|
||||||
|
}
|
||||||
|
blob, err := dao.GetBlob(mbi.blobInfo.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if blob == nil {
|
||||||
|
return fmt.Errorf("the blob in the mount request with digest: %s doesn't exist", mbi.blobInfo.Digest)
|
||||||
|
}
|
||||||
|
mbi.blobInfo.Size = blob.Size
|
||||||
|
con, err := util.GetRegRedisCon()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := requireQuota(con, mbi.blobInfo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.BBInfokKey, mbi.blobInfo)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleResponse ...
|
||||||
|
func (mbi *MountBlobInterceptor) HandleResponse(rw util.CustomResponseWriter, req *http.Request) {
|
||||||
|
if err := HandleBlobCommon(rw, req); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
85
src/core/middlewares/sizequota/mountblob_test.go
Normal file
85
src/core/middlewares/sizequota/mountblob_test.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewMountBlobInterceptor(t *testing.T) {
|
||||||
|
blobinfo := &util.BlobInfo{}
|
||||||
|
blobinfo.Repository = "TestNewMountBlobInterceptor/latest"
|
||||||
|
|
||||||
|
bi := NewMountBlobInterceptor(blobinfo)
|
||||||
|
assert.NotNil(t, bi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountBlobHandleRequest(t *testing.T) {
|
||||||
|
blobInfo := util.BlobInfo{
|
||||||
|
Repository: "TestHandleRequest/test",
|
||||||
|
Digest: "sha256:TestHandleRequest1234",
|
||||||
|
ContentType: "ContentType",
|
||||||
|
Size: 101,
|
||||||
|
Exist: false,
|
||||||
|
}
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
bi := NewMountBlobInterceptor(&blobInfo)
|
||||||
|
assert.NotNil(t, bi.HandleRequest(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountBlobHandleResponse(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
blobInfo := util.BlobInfo{
|
||||||
|
Repository: "TestHandleResponse/test",
|
||||||
|
Digest: "sha256:TestHandleResponseabcdf12345678sdfefeg1246",
|
||||||
|
ContentType: "ContentType",
|
||||||
|
Size: 101,
|
||||||
|
Exist: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
customResW := util.CustomResponseWriter{ResponseWriter: rw}
|
||||||
|
customResW.WriteHeader(201)
|
||||||
|
|
||||||
|
lock, err := tryLockBlob(con, &blobInfo)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
blobInfo.DigestLock = lock
|
||||||
|
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.BBInfokKey, &blobInfo)))
|
||||||
|
|
||||||
|
bi := NewMountBlobInterceptor(&blobInfo)
|
||||||
|
assert.NotNil(t, bi)
|
||||||
|
|
||||||
|
bi.HandleResponse(customResW, req)
|
||||||
|
|
||||||
|
}
|
86
src/core/middlewares/sizequota/patchblob.go
Normal file
86
src/core/middlewares/sizequota/patchblob.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PatchBlobInterceptor ...
|
||||||
|
type PatchBlobInterceptor struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPatchBlobInterceptor ...
|
||||||
|
func NewPatchBlobInterceptor() *PatchBlobInterceptor {
|
||||||
|
return &PatchBlobInterceptor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest do nothing for patch blob, just let the request to proxy.
|
||||||
|
func (pbi *PatchBlobInterceptor) HandleRequest(req *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleResponse record the upload process with Range attribute, set it into redis with UUID as the key
|
||||||
|
func (pbi *PatchBlobInterceptor) HandleResponse(rw util.CustomResponseWriter, req *http.Request) {
|
||||||
|
if rw.Status() != http.StatusAccepted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
con, err := util.GetRegRedisCon()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
uuid := rw.Header().Get("Docker-Upload-UUID")
|
||||||
|
if uuid == "" {
|
||||||
|
log.Errorf("no UUID in the patch blob response, the request path %s ", req.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range: Range indicating the current progress of the upload.
|
||||||
|
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#get-blob-upload
|
||||||
|
patchRange := rw.Header().Get("Range")
|
||||||
|
if uuid == "" {
|
||||||
|
log.Errorf("no Range in the patch blob response, the request path %s ", req.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endRange := strings.Split(patchRange, "-")[1]
|
||||||
|
size, err := strconv.ParseInt(endRange, 10, 64)
|
||||||
|
// docker registry did '-1' in the response
|
||||||
|
if size > 0 {
|
||||||
|
size = size + 1
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
success, err := util.SetBunkSize(con, uuid, size)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
// ToDo discuss what to do here.
|
||||||
|
log.Warningf(" T_T: Fail to set bunk: %s size: %d in redis, it causes unable to set correct quota for the artifact.", uuid, size)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
42
src/core/middlewares/sizequota/patchblob_test.go
Normal file
42
src/core/middlewares/sizequota/patchblob_test.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPatchBlobInterceptor(t *testing.T) {
|
||||||
|
bi := NewPatchBlobInterceptor()
|
||||||
|
assert.NotNil(t, bi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchBlobHandleRequest(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
bi := NewPatchBlobInterceptor()
|
||||||
|
assert.Nil(t, bi.HandleRequest(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchBlobHandleResponse(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
customResW := util.CustomResponseWriter{ResponseWriter: rw}
|
||||||
|
customResW.WriteHeader(400)
|
||||||
|
NewPatchBlobInterceptor().HandleResponse(customResW, req)
|
||||||
|
}
|
83
src/core/middlewares/sizequota/putblob.go
Normal file
83
src/core/middlewares/sizequota/putblob.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PutBlobInterceptor ...
|
||||||
|
type PutBlobInterceptor struct {
|
||||||
|
blobInfo *util.BlobInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPutBlobInterceptor ...
|
||||||
|
func NewPutBlobInterceptor(blobInfo *util.BlobInfo) *PutBlobInterceptor {
|
||||||
|
return &PutBlobInterceptor{
|
||||||
|
blobInfo: blobInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
func (pbi *PutBlobInterceptor) HandleRequest(req *http.Request) error {
|
||||||
|
// the redis connection will be closed in the put response.
|
||||||
|
con, err := util.GetRegRedisCon()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if pbi.blobInfo.UUID != "" {
|
||||||
|
_, err := rmBlobUploadUUID(con, pbi.blobInfo.UUID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("error occurred when remove UUID for blob, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dgstStr := req.FormValue("digest")
|
||||||
|
if dgstStr == "" {
|
||||||
|
return errors.New("blob digest missing")
|
||||||
|
}
|
||||||
|
dgst, err := digest.Parse(dgstStr)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("blob digest parsing failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pbi.blobInfo.Digest = dgst.String()
|
||||||
|
pbi.blobInfo.UUID = getUUID(req.URL.Path)
|
||||||
|
size, err := util.GetBlobSize(con, pbi.blobInfo.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pbi.blobInfo.Size = size
|
||||||
|
if err := requireQuota(con, pbi.blobInfo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.BBInfokKey, pbi.blobInfo)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleResponse ...
|
||||||
|
func (pbi *PutBlobInterceptor) HandleResponse(rw util.CustomResponseWriter, req *http.Request) {
|
||||||
|
if err := HandleBlobCommon(rw, req); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
80
src/core/middlewares/sizequota/putblob_test.go
Normal file
80
src/core/middlewares/sizequota/putblob_test.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPutBlobInterceptor(t *testing.T) {
|
||||||
|
blobinfo := &util.BlobInfo{}
|
||||||
|
blobinfo.Repository = "TestNewPutBlobInterceptor/latest"
|
||||||
|
|
||||||
|
bi := NewPutBlobInterceptor(blobinfo)
|
||||||
|
assert.NotNil(t, bi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutBlobHandleRequest(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
blobinfo := &util.BlobInfo{}
|
||||||
|
blobinfo.Repository = "TestPutBlobHandleRequest/latest"
|
||||||
|
|
||||||
|
bi := NewPutBlobInterceptor(blobinfo)
|
||||||
|
assert.NotNil(t, bi.HandleRequest(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutBlobHandleResponse(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
blobInfo := util.BlobInfo{
|
||||||
|
Repository: "TestPutBlobHandleResponse/test",
|
||||||
|
Digest: "sha256:TestPutBlobHandleResponseabcdf12345678sdfefeg1246",
|
||||||
|
ContentType: "ContentType",
|
||||||
|
Size: 101,
|
||||||
|
Exist: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
customResW := util.CustomResponseWriter{ResponseWriter: rw}
|
||||||
|
customResW.WriteHeader(201)
|
||||||
|
|
||||||
|
lock, err := tryLockBlob(con, &blobInfo)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
blobInfo.DigestLock = lock
|
||||||
|
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.BBInfokKey, &blobInfo)))
|
||||||
|
|
||||||
|
bi := NewPutBlobInterceptor(&blobInfo)
|
||||||
|
assert.NotNil(t, bi)
|
||||||
|
|
||||||
|
bi.HandleResponse(customResW, req)
|
||||||
|
}
|
102
src/core/middlewares/sizequota/putmanifest.go
Normal file
102
src/core/middlewares/sizequota/putmanifest.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/distribution"
|
||||||
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PutManifestInterceptor ...
|
||||||
|
type PutManifestInterceptor struct {
|
||||||
|
blobInfo *util.BlobInfo
|
||||||
|
mfInfo *util.MfInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPutManifestInterceptor ...
|
||||||
|
func NewPutManifestInterceptor(blobInfo *util.BlobInfo, mfInfo *util.MfInfo) *PutManifestInterceptor {
|
||||||
|
return &PutManifestInterceptor{
|
||||||
|
blobInfo: blobInfo,
|
||||||
|
mfInfo: mfInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleRequest ...
|
||||||
|
func (pmi *PutManifestInterceptor) HandleRequest(req *http.Request) error {
|
||||||
|
mediaType := req.Header.Get("Content-Type")
|
||||||
|
if mediaType == schema1.MediaTypeManifest ||
|
||||||
|
mediaType == schema1.MediaTypeSignedManifest ||
|
||||||
|
mediaType == schema2.MediaTypeManifest {
|
||||||
|
|
||||||
|
con, err := util.GetRegRedisCon()
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("failed to get registry redis connection, %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error occurred when to copy manifest body %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
||||||
|
manifest, desc, err := distribution.UnmarshalManifest(mediaType, data)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error occurred when to Unmarshal Manifest %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
projectID, err := util.GetProjectID(strings.Split(pmi.mfInfo.Repository, "/")[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error occurred when to get project ID %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pmi.mfInfo.ProjectID = projectID
|
||||||
|
pmi.mfInfo.Refrerence = manifest.References()
|
||||||
|
pmi.mfInfo.Digest = desc.Digest.String()
|
||||||
|
pmi.blobInfo.ProjectID = projectID
|
||||||
|
pmi.blobInfo.Digest = desc.Digest.String()
|
||||||
|
pmi.blobInfo.Size = desc.Size
|
||||||
|
pmi.blobInfo.ContentType = mediaType
|
||||||
|
|
||||||
|
if err := requireQuota(con, pmi.blobInfo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.MFInfokKey, pmi.mfInfo)))
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.BBInfokKey, pmi.blobInfo)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unsupported content type for manifest: %s", mediaType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleResponse ...
|
||||||
|
func (pmi *PutManifestInterceptor) HandleResponse(rw util.CustomResponseWriter, req *http.Request) {
|
||||||
|
if err := HandleBlobCommon(rw, req); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
92
src/core/middlewares/sizequota/putmanifest_test.go
Normal file
92
src/core/middlewares/sizequota/putmanifest_test.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package sizequota
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPutManifestInterceptor(t *testing.T) {
|
||||||
|
blobinfo := &util.BlobInfo{}
|
||||||
|
blobinfo.Repository = "TestNewPutManifestInterceptor/latest"
|
||||||
|
|
||||||
|
mfinfo := &util.MfInfo{
|
||||||
|
Repository: "TestNewPutManifestInterceptor",
|
||||||
|
}
|
||||||
|
|
||||||
|
mi := NewPutManifestInterceptor(blobinfo, mfinfo)
|
||||||
|
assert.NotNil(t, mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutManifestHandleRequest(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
blobinfo := &util.BlobInfo{}
|
||||||
|
blobinfo.Repository = "TestPutManifestHandleRequest/latest"
|
||||||
|
|
||||||
|
mfinfo := &util.MfInfo{
|
||||||
|
Repository: "TestPutManifestHandleRequest",
|
||||||
|
}
|
||||||
|
|
||||||
|
mi := NewPutManifestInterceptor(blobinfo, mfinfo)
|
||||||
|
assert.NotNil(t, mi.HandleRequest(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutManifestHandleResponse(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
|
blobInfo := util.BlobInfo{
|
||||||
|
Repository: "TestPutManifestandleResponse/test",
|
||||||
|
Digest: "sha256:TestPutManifestandleResponseabcdf12345678sdfefeg1246",
|
||||||
|
ContentType: "ContentType",
|
||||||
|
Size: 101,
|
||||||
|
Exist: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
mfinfo := util.MfInfo{
|
||||||
|
Repository: "TestPutManifestandleResponse",
|
||||||
|
}
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
customResW := util.CustomResponseWriter{ResponseWriter: rw}
|
||||||
|
customResW.WriteHeader(201)
|
||||||
|
|
||||||
|
lock, err := tryLockBlob(con, &blobInfo)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
blobInfo.DigestLock = lock
|
||||||
|
|
||||||
|
*req = *(req.WithContext(context.WithValue(req.Context(), util.BBInfokKey, &blobInfo)))
|
||||||
|
|
||||||
|
bi := NewPutManifestInterceptor(&blobInfo, &mfinfo)
|
||||||
|
assert.NotNil(t, bi)
|
||||||
|
|
||||||
|
bi.HandleResponse(customResW, req)
|
||||||
|
}
|
28
src/core/middlewares/util/reginteceptor.go
Normal file
28
src/core/middlewares/util/reginteceptor.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegInterceptor ...
|
||||||
|
type RegInterceptor interface {
|
||||||
|
// HandleRequest ...
|
||||||
|
HandleRequest(req *http.Request) error
|
||||||
|
|
||||||
|
// HandleResponse won't return any error
|
||||||
|
HandleResponse(rw CustomResponseWriter, req *http.Request)
|
||||||
|
}
|
59
src/core/middlewares/util/response.go
Normal file
59
src/core/middlewares/util/response.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomResponseWriter write the response code into the status
|
||||||
|
type CustomResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
wroteHeader bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCustomResponseWriter ...
|
||||||
|
func NewCustomResponseWriter(w http.ResponseWriter) *CustomResponseWriter {
|
||||||
|
return &CustomResponseWriter{ResponseWriter: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status ...
|
||||||
|
func (w *CustomResponseWriter) Status() int {
|
||||||
|
return w.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header ...
|
||||||
|
func (w CustomResponseWriter) Header() http.Header {
|
||||||
|
return w.ResponseWriter.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write ...
|
||||||
|
func (w *CustomResponseWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader ...
|
||||||
|
func (w *CustomResponseWriter) WriteHeader(code int) {
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
if w.wroteHeader {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.status = code
|
||||||
|
w.wroteHeader = true
|
||||||
|
}
|
29
src/core/middlewares/util/response_test.go
Normal file
29
src/core/middlewares/util/response_test.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright Project Harbor Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCustomResponseWriter(t *testing.T) {
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
customResW := CustomResponseWriter{ResponseWriter: rw}
|
||||||
|
customResW.WriteHeader(501)
|
||||||
|
assert.Equal(t, customResW.Status(), 501)
|
||||||
|
}
|
@ -16,7 +16,11 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/quota"
|
"github.com/goharbor/harbor/src/common/quota"
|
||||||
"github.com/goharbor/harbor/src/common/utils/clair"
|
"github.com/goharbor/harbor/src/common/utils/clair"
|
||||||
@ -28,11 +32,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
type contextKey string
|
||||||
|
|
||||||
|
// ErrRequireQuota ...
|
||||||
|
var ErrRequireQuota = errors.New("cannot get quota on project for request")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
manifestURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})`
|
manifestURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})`
|
||||||
blobURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)blobs/uploads/`
|
blobURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)blobs/uploads/`
|
||||||
@ -42,7 +51,16 @@ const (
|
|||||||
// TODO: temp solution, remove after vmware/harbor#2242 is resolved.
|
// TODO: temp solution, remove after vmware/harbor#2242 is resolved.
|
||||||
TokenUsername = "harbor-core"
|
TokenUsername = "harbor-core"
|
||||||
// MFInfokKey the context key for image tag redis lock
|
// MFInfokKey the context key for image tag redis lock
|
||||||
MFInfokKey = contextKey("ManifestLock")
|
MFInfokKey = contextKey("ManifestInfo")
|
||||||
|
// BBInfokKey the context key for image tag redis lock
|
||||||
|
BBInfokKey = contextKey("BlobInfo")
|
||||||
|
|
||||||
|
// DialConnectionTimeout ...
|
||||||
|
DialConnectionTimeout = 30 * time.Second
|
||||||
|
// DialReadTimeout ...
|
||||||
|
DialReadTimeout = time.Minute + 10*time.Second
|
||||||
|
// DialWriteTimeout ...
|
||||||
|
DialWriteTimeout = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageInfo ...
|
// ImageInfo ...
|
||||||
@ -53,6 +71,24 @@ type ImageInfo struct {
|
|||||||
Digest string
|
Digest string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BlobInfo ...
|
||||||
|
type BlobInfo struct {
|
||||||
|
UUID string
|
||||||
|
ProjectID int64
|
||||||
|
ContentType string
|
||||||
|
Size int64
|
||||||
|
Repository string
|
||||||
|
Tag string
|
||||||
|
|
||||||
|
// Exist is to index the existing of the manifest in DB. If false, it's an new image for uploading.
|
||||||
|
Exist bool
|
||||||
|
|
||||||
|
Digest string
|
||||||
|
DigestLock *common_redis.Mutex
|
||||||
|
// Quota is the resource applied for the manifest upload request.
|
||||||
|
Quota *quota.ResourceList
|
||||||
|
}
|
||||||
|
|
||||||
// MfInfo ...
|
// MfInfo ...
|
||||||
type MfInfo struct {
|
type MfInfo struct {
|
||||||
// basic information of a manifest
|
// basic information of a manifest
|
||||||
@ -133,6 +169,24 @@ func MatchPutBlobURL(req *http.Request) (bool, string) {
|
|||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchPatchBlobURL ...
|
||||||
|
func MatchPatchBlobURL(req *http.Request) (bool, string) {
|
||||||
|
if req.Method != http.MethodPatch {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
re, err := regexp.Compile(blobURLPattern)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error to match put blob url, %v", err)
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
s := re.FindStringSubmatch(req.URL.Path)
|
||||||
|
if len(s) == 2 {
|
||||||
|
s[1] = strings.TrimSuffix(s[1], "/")
|
||||||
|
return true, s[1]
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
// MatchPullManifest checks if the request looks like a request to pull manifest. If it is returns the image and tag/sha256 digest as 2nd and 3rd return values
|
// MatchPullManifest checks if the request looks like a request to pull manifest. If it is returns the image and tag/sha256 digest as 2nd and 3rd return values
|
||||||
func MatchPullManifest(req *http.Request) (bool, string, string) {
|
func MatchPullManifest(req *http.Request) (bool, string, string) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
@ -149,6 +203,33 @@ func MatchPushManifest(req *http.Request) (bool, string, string) {
|
|||||||
return MatchManifestURL(req)
|
return MatchManifestURL(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchMountBlobURL POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name>
|
||||||
|
// If match, will return repo, mount and from as the 2nd, 3th and 4th.
|
||||||
|
func MatchMountBlobURL(req *http.Request) (bool, string, string, string) {
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
return false, "", "", ""
|
||||||
|
}
|
||||||
|
re, err := regexp.Compile(blobURLPattern)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error to match post blob url, %v", err)
|
||||||
|
return false, "", "", ""
|
||||||
|
}
|
||||||
|
s := re.FindStringSubmatch(req.URL.Path)
|
||||||
|
if len(s) == 2 {
|
||||||
|
s[1] = strings.TrimSuffix(s[1], "/")
|
||||||
|
mount := req.FormValue("mount")
|
||||||
|
if mount == "" {
|
||||||
|
return false, "", "", ""
|
||||||
|
}
|
||||||
|
from := req.FormValue("from")
|
||||||
|
if from == "" {
|
||||||
|
return false, "", "", ""
|
||||||
|
}
|
||||||
|
return true, s[1], mount, from
|
||||||
|
}
|
||||||
|
return false, "", "", ""
|
||||||
|
}
|
||||||
|
|
||||||
// CopyResp ...
|
// CopyResp ...
|
||||||
func CopyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) {
|
func CopyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) {
|
||||||
for k, v := range rec.Header() {
|
for k, v := range rec.Header() {
|
||||||
@ -218,3 +299,79 @@ func NewPMSPolicyChecker(pm promgr.ProjectManager) PolicyChecker {
|
|||||||
func GetPolicyChecker() PolicyChecker {
|
func GetPolicyChecker() PolicyChecker {
|
||||||
return NewPMSPolicyChecker(config.GlobalProjectMgr)
|
return NewPMSPolicyChecker(config.GlobalProjectMgr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TryRequireQuota ...
|
||||||
|
func TryRequireQuota(projectID int64, quotaRes *quota.ResourceList) error {
|
||||||
|
quotaMgr, err := quota.NewManager("project", strconv.FormatInt(projectID, 10))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error occurred when to new quota manager %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := quotaMgr.AddResources(*quotaRes); err != nil {
|
||||||
|
log.Errorf("cannot get quota for the project resource: %d, err: %v", projectID, err)
|
||||||
|
return ErrRequireQuota
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryFreeQuota used to release resource for failure case
|
||||||
|
func TryFreeQuota(projectID int64, qres *quota.ResourceList) bool {
|
||||||
|
quotaMgr, err := quota.NewManager("project", strconv.FormatInt(projectID, 10))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error occurred when to new quota manager %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := quotaMgr.SubtractResources(*qres); err != nil {
|
||||||
|
log.Errorf("cannot release quota for the project resource: %d, err: %v", projectID, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlobSize blob size with UUID in redis
|
||||||
|
func GetBlobSize(conn redis.Conn, uuid string) (int64, error) {
|
||||||
|
exists, err := redis.Int(conn.Do("EXISTS", uuid))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if exists == 1 {
|
||||||
|
size, err := redis.Int64(conn.Do("GET", uuid))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBunkSize sets the temp size for blob bunk with its uuid.
|
||||||
|
func SetBunkSize(conn redis.Conn, uuid string, size int64) (bool, error) {
|
||||||
|
setRes, err := redis.String(conn.Do("SET", uuid, size))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return setRes == "OK", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectID ...
|
||||||
|
func GetProjectID(name string) (int64, error) {
|
||||||
|
project, err := dao.GetProjectByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if project != nil {
|
||||||
|
return project.ProjectID, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("project %s is not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRegRedisCon ...
|
||||||
|
func GetRegRedisCon() (redis.Conn, error) {
|
||||||
|
return redis.DialURL(
|
||||||
|
config.GetRedisOfRegURL(),
|
||||||
|
redis.DialConnectTimeout(DialConnectionTimeout),
|
||||||
|
redis.DialReadTimeout(DialReadTimeout),
|
||||||
|
redis.DialWriteTimeout(DialWriteTimeout),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
|
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
|
||||||
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
||||||
@ -23,19 +24,26 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/goharbor/harbor/src/common/quota"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var endpoint = "10.117.4.142"
|
var endpoint = "10.117.4.142"
|
||||||
var notaryServer *httptest.Server
|
var notaryServer *httptest.Server
|
||||||
|
|
||||||
|
const testingRedisHost = "REDIS_HOST"
|
||||||
|
|
||||||
var admiralEndpoint = "http://127.0.0.1:8282"
|
var admiralEndpoint = "http://127.0.0.1:8282"
|
||||||
var token = ""
|
var token = ""
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
testutils.InitDatabaseFromEnv()
|
||||||
notaryServer = notarytest.NewNotaryServer(endpoint)
|
notaryServer = notarytest.NewNotaryServer(endpoint)
|
||||||
defer notaryServer.Close()
|
defer notaryServer.Close()
|
||||||
var defaultConfig = map[string]interface{}{
|
var defaultConfig = map[string]interface{}{
|
||||||
@ -107,6 +115,40 @@ func TestMatchPutBlob(t *testing.T) {
|
|||||||
assert.False(res3, "%s %v is not a request to put blob", req3.Method, req3.URL)
|
assert.False(res3, "%s %v is not a request to put blob", req3.Method, req3.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMatchMountBlobURL(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
req1, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/library/ubuntu/blobs/uploads/?mount=digtest123&from=testrepo", nil)
|
||||||
|
res1, repo1, mount, from := MatchMountBlobURL(req1)
|
||||||
|
assert.True(res1, "%s %v is not a request to mount blob", req1.Method, req1.URL)
|
||||||
|
assert.Equal("library/ubuntu", repo1)
|
||||||
|
assert.Equal("digtest123", mount)
|
||||||
|
assert.Equal("testrepo", from)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("PATCH", "http://127.0.0.1:5000/v2/library/ubuntu/blobs/uploads/?mount=digtest123&from=testrepo", nil)
|
||||||
|
res2, _, _, _ := MatchMountBlobURL(req2)
|
||||||
|
assert.False(res2, "%s %v is a request to mount blob", req2.Method, req2.URL)
|
||||||
|
|
||||||
|
req3, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/blobs/uploads/?mount=digtest123&from=testrepo", nil)
|
||||||
|
res3, _, _, _ := MatchMountBlobURL(req3)
|
||||||
|
assert.False(res3, "%s %v is not a request to put blob", req3.Method, req3.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchBlobURL(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
req1, _ := http.NewRequest("PATCH", "http://127.0.0.1:5000/v2/library/ubuntu/blobs/uploads/1234-1234-abcd", nil)
|
||||||
|
res1, repo1 := MatchPatchBlobURL(req1)
|
||||||
|
assert.True(res1, "%s %v is not a request to patch blob", req1.Method, req1.URL)
|
||||||
|
assert.Equal("library/ubuntu", repo1)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/library/ubuntu/blobs/uploads/1234-1234-abcd", nil)
|
||||||
|
res2, _ := MatchPatchBlobURL(req2)
|
||||||
|
assert.False(res2, "%s %v is a request to patch blob", req2.Method, req2.URL)
|
||||||
|
|
||||||
|
req3, _ := http.NewRequest("PUT", "http://127.0.0.1:5000/v2/library/ubuntu/blobs/uploads/?mount=digtest123&from=testrepo", nil)
|
||||||
|
res3, _ := MatchPatchBlobURL(req3)
|
||||||
|
assert.False(res3, "%s %v is not a request to patch blob", req3.Method, req3.URL)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMatchPushManifest(t *testing.T) {
|
func TestMatchPushManifest(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
req1, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
req1, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil)
|
||||||
@ -170,7 +212,6 @@ func TestPMSPolicyChecker(t *testing.T) {
|
|||||||
if err := config.Init(); err != nil {
|
if err := config.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
testutils.InitDatabaseFromEnv()
|
|
||||||
|
|
||||||
config.Upload(defaultConfigAdmiral)
|
config.Upload(defaultConfigAdmiral)
|
||||||
|
|
||||||
@ -218,3 +259,84 @@ func TestMarshalError(t *testing.T) {
|
|||||||
js2 := MarshalError("DENIED", "The action is denied")
|
js2 := MarshalError("DENIED", "The action is denied")
|
||||||
assert.Equal("{\"errors\":[{\"code\":\"DENIED\",\"message\":\"The action is denied\",\"detail\":\"The action is denied\"}]}", js2)
|
assert.Equal("{\"errors\":[{\"code\":\"DENIED\",\"message\":\"The action is denied\",\"detail\":\"The action is denied\"}]}", js2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTryRequireQuota(t *testing.T) {
|
||||||
|
quotaRes := "a.ResourceList{
|
||||||
|
quota.ResourceStorage: 100,
|
||||||
|
}
|
||||||
|
err := TryRequireQuota(1, quotaRes)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTryFreeQuota(t *testing.T) {
|
||||||
|
quotaRes := "a.ResourceList{
|
||||||
|
quota.ResourceStorage: 1,
|
||||||
|
}
|
||||||
|
success := TryFreeQuota(1, quotaRes)
|
||||||
|
assert.True(t, success)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBlobSize(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
size, err := GetBlobSize(con, "test-TestGetBlobSize")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, size, int64(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetBunkSize(t *testing.T) {
|
||||||
|
con, err := redis.Dial(
|
||||||
|
"tcp",
|
||||||
|
fmt.Sprintf("%s:%d", getRedisHost(), 6379),
|
||||||
|
redis.DialConnectTimeout(30*time.Second),
|
||||||
|
redis.DialReadTimeout(time.Minute+10*time.Second),
|
||||||
|
redis.DialWriteTimeout(10*time.Second),
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer con.Close()
|
||||||
|
|
||||||
|
size, err := GetBlobSize(con, "TestSetBunkSize")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, size, int64(0))
|
||||||
|
|
||||||
|
_, err = SetBunkSize(con, "TestSetBunkSize", 123)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
size1, err := GetBlobSize(con, "TestSetBunkSize")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, size1, int64(123))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProjectID(t *testing.T) {
|
||||||
|
name := "project_for_TestGetProjectID"
|
||||||
|
project := models.Project{
|
||||||
|
OwnerID: 1,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := dao.AddProject(project)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to add project: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idget, err := GetProjectID(name)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, id, idget)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRedisHost() string {
|
||||||
|
redisHost := os.Getenv(testingRedisHost)
|
||||||
|
if redisHost == "" {
|
||||||
|
redisHost = "127.0.0.1" // for local test
|
||||||
|
}
|
||||||
|
|
||||||
|
return redisHost
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user