mirror of https://github.com/goharbor/harbor.git
Add max_upstream_conn parameter for each proxy_cache project (#22348)
Build Package Workflow / BUILD_PACKAGE (push) Has been cancelled
Details
Code scanning - action / CodeQL-Build (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-core, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-core, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-db, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-db, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-exporter, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-exporter, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-jobservice, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-jobservice, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-log, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-log, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-portal, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-portal, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-registryctl, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-registryctl, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (prepare, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (prepare, v2.12.0-dev) (push) Has been cancelled
Details
CONFORMANCE_TEST / CONFORMANCE_TEST (push) Has been cancelled
Details
Housekeeping - Close stale issues and PRs / stale (push) Has been cancelled
Details
Build Package Workflow / BUILD_PACKAGE (push) Has been cancelled
Details
Code scanning - action / CodeQL-Build (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-core, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-core, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-db, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-db, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-exporter, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-exporter, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-jobservice, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-jobservice, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-log, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-log, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-portal, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-portal, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-registryctl, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (harbor-registryctl, v2.12.0-dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (prepare, dev) (push) Has been cancelled
Details
Trivy Nightly Scan / Trivy Scan nightly (prepare, v2.12.0-dev) (push) Has been cancelled
Details
CONFORMANCE_TEST / CONFORMANCE_TEST (push) Has been cancelled
Details
Housekeeping - Close stale issues and PRs / stale (push) Has been cancelled
Details
limit the proxy connection to upstream registry Signed-off-by: stonezdj <stonezdj@gmail.com>
This commit is contained in:
parent
4da6070872
commit
c004f2d3e6
|
@ -7321,6 +7321,10 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
description: 'The bandwidth limit of proxy cache, in Kbps (kilobits per second). It limits the communication between Harbor and the upstream registry, not the client and the Harbor.'
|
description: 'The bandwidth limit of proxy cache, in Kbps (kilobits per second). It limits the communication between Harbor and the upstream registry, not the client and the Harbor.'
|
||||||
x-nullable: true
|
x-nullable: true
|
||||||
|
max_upstream_conn:
|
||||||
|
type: string
|
||||||
|
description: 'The max connection per artifact to the upstream registry in current proxy cache project, if it is -1, no limit to upstream registry connections'
|
||||||
|
x-nullable: true
|
||||||
ProjectSummary:
|
ProjectSummary:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
|
@ -25,4 +25,5 @@ const (
|
||||||
ProMetaReuseSysCVEAllowlist = "reuse_sys_cve_allowlist"
|
ProMetaReuseSysCVEAllowlist = "reuse_sys_cve_allowlist"
|
||||||
ProMetaAutoSBOMGen = "auto_sbom_generation"
|
ProMetaAutoSBOMGen = "auto_sbom_generation"
|
||||||
ProMetaProxySpeed = "proxy_speed_kb"
|
ProMetaProxySpeed = "proxy_speed_kb"
|
||||||
|
ProMetaMaxUpstreamConn = "max_upstream_conn"
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/lib/orm"
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
allowlist "github.com/goharbor/harbor/src/pkg/allowlist/models"
|
allowlist "github.com/goharbor/harbor/src/pkg/allowlist/models"
|
||||||
)
|
)
|
||||||
|
@ -169,6 +170,20 @@ func (p *Project) ProxyCacheSpeed() int32 {
|
||||||
return int32(speedInt)
|
return int32(speedInt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaxUpstreamConnection ...
|
||||||
|
func (p *Project) MaxUpstreamConnection() int {
|
||||||
|
countVal, exist := p.GetMetadata(ProMetaMaxUpstreamConn)
|
||||||
|
if !exist {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
cnt, err := strconv.ParseInt(countVal, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("failed th parse the max_upstream_conn, val:%s error %v", countVal, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(cnt)
|
||||||
|
}
|
||||||
|
|
||||||
// FilterByPublic returns orm.QuerySeter with public filter
|
// FilterByPublic returns orm.QuerySeter with public filter
|
||||||
func (p *Project) FilterByPublic(_ context.Context, qs orm.QuerySeter, _ string, value any) orm.QuerySeter {
|
func (p *Project) FilterByPublic(_ context.Context, qs orm.QuerySeter, _ string, value any) orm.QuerySeter {
|
||||||
subQuery := `SELECT project_id FROM project_metadata WHERE name = 'public' AND value = '%s'`
|
subQuery := `SELECT project_id FROM project_metadata WHERE name = 'public' AND value = '%s'`
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
// 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 connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConLimiter is used to limit the number of connections to the upstream service
|
||||||
|
type ConnLimiter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limiter is a global connection limiter instance
|
||||||
|
var Limiter = &ConnLimiter{}
|
||||||
|
|
||||||
|
// Used to compare and increase connection number in redis
|
||||||
|
//
|
||||||
|
// KEYS[1]: key of max_conn_upstream
|
||||||
|
// ARGV[1]: max connection limit
|
||||||
|
var increaseWithLimitText = `
|
||||||
|
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
|
||||||
|
local max = tonumber(ARGV[1])
|
||||||
|
|
||||||
|
if current + 1 <= max then
|
||||||
|
redis.call('INCRBY', KEYS[1], 1)
|
||||||
|
redis.call('EXPIRE', KEYS[1], 3600) -- set expire to avoid always lock
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
`
|
||||||
|
|
||||||
|
var acquireScript = redis.NewScript(increaseWithLimitText)
|
||||||
|
|
||||||
|
// Acquire tries to acquire a connection, returns true if successful
|
||||||
|
func (c *ConnLimiter) Acquire(ctx context.Context, rdb *redis.Client, key string, limit int) bool {
|
||||||
|
result, err := acquireScript.Run(ctx, rdb, []string{key}, fmt.Sprintf("%v", limit)).Int()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get the connection lock in redis, error %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
log.Debugf("Acquire script result is %d", result)
|
||||||
|
return result == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var decreaseText = `
|
||||||
|
local val = tonumber(redis.call("GET", KEYS[1]) or "0")
|
||||||
|
if val > 0 then
|
||||||
|
redis.call("DECR", KEYS[1])
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
`
|
||||||
|
|
||||||
|
var decreaseScript = redis.NewScript(decreaseText)
|
||||||
|
|
||||||
|
// Release releases a connection in redis
|
||||||
|
func (c *ConnLimiter) Release(ctx context.Context, rdb *redis.Client, key string) {
|
||||||
|
_, err := decreaseScript.Run(ctx, rdb, []string{key}).Int()
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("release connection failed:%v", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConnLimiter_Acquire_Release(t *testing.T) {
|
||||||
|
redisAddress := os.Getenv("REDIS_HOST")
|
||||||
|
redisHost := "localhost"
|
||||||
|
if len(redisAddress) > 0 {
|
||||||
|
redisHost = redisAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:6379", redisHost), // Redis server address
|
||||||
|
Password: "", // No password set
|
||||||
|
DB: 0, // Use default DB
|
||||||
|
})
|
||||||
|
key := "test_max_connection_key"
|
||||||
|
maxConn := 10
|
||||||
|
for range 10 {
|
||||||
|
result := Limiter.Acquire(ctx, rdb, key, maxConn)
|
||||||
|
assert.True(t, result)
|
||||||
|
}
|
||||||
|
// after max connection reached, it should be false
|
||||||
|
result2 := Limiter.Acquire(ctx, rdb, key, maxConn)
|
||||||
|
assert.False(t, result2)
|
||||||
|
|
||||||
|
for range 10 {
|
||||||
|
Limiter.Release(ctx, rdb, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// connection in redis should be 0 finally
|
||||||
|
n, err := rdb.Get(ctx, key).Int()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 0, n)
|
||||||
|
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -33,18 +34,21 @@ import (
|
||||||
httpLib "github.com/goharbor/harbor/src/lib/http"
|
httpLib "github.com/goharbor/harbor/src/lib/http"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/lib/orm"
|
"github.com/goharbor/harbor/src/lib/orm"
|
||||||
|
"github.com/goharbor/harbor/src/lib/redis"
|
||||||
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/proxy/connection"
|
||||||
"github.com/goharbor/harbor/src/pkg/reg/model"
|
"github.com/goharbor/harbor/src/pkg/reg/model"
|
||||||
"github.com/goharbor/harbor/src/server/middleware"
|
"github.com/goharbor/harbor/src/server/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
contentLength = "Content-Length"
|
contentLength = "Content-Length"
|
||||||
contentType = "Content-Type"
|
contentType = "Content-Type"
|
||||||
dockerContentDigest = "Docker-Content-Digest"
|
dockerContentDigest = "Docker-Content-Digest"
|
||||||
etag = "Etag"
|
etag = "Etag"
|
||||||
ensureTagInterval = 10 * time.Second
|
ensureTagInterval = 10 * time.Second
|
||||||
ensureTagMaxRetry = 60
|
ensureTagMaxRetry = 60
|
||||||
|
upstreamRegistryLimitOnProject = "UPSTREAM_REGISTRY_LIMIT_ON_PROJECT" // if UPSTREAM_REGISTRY_LIMIT_ON_PROJECT is true, the upstream registry connection is based on project level, by default it is artifact level
|
||||||
)
|
)
|
||||||
|
|
||||||
var tooManyRequestsError = errors.New("too many requests to upstream registry").WithCode(errors.RateLimitCode)
|
var tooManyRequestsError = errors.New("too many requests to upstream registry").WithCode(errors.RateLimitCode)
|
||||||
|
@ -99,6 +103,22 @@ func handleBlob(w http.ResponseWriter, r *http.Request, next http.Handler) error
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.MaxUpstreamConnection() > 0 {
|
||||||
|
client, err := redis.GetHarborClient()
|
||||||
|
if err != nil {
|
||||||
|
return errors.NewErrs(err)
|
||||||
|
}
|
||||||
|
key := upstreamRegistryConnectionKey(art)
|
||||||
|
log.Debugf("handle blob, upstream registry connection limit key: %s", key)
|
||||||
|
if !connection.Limiter.Acquire(ctx, client, key, p.MaxUpstreamConnection()) {
|
||||||
|
log.Infof("current connection exceed max connections to upstream registry")
|
||||||
|
// send http code 429 to client
|
||||||
|
return tooManyRequestsError
|
||||||
|
}
|
||||||
|
defer connection.Limiter.Release(context.Background(), client, key) // use background context in defer to avoid been canceled
|
||||||
|
}
|
||||||
|
|
||||||
size, reader, err := proxyCtl.ProxyBlob(ctx, p, art)
|
size, reader, err := proxyCtl.ProxyBlob(ctx, p, art)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -173,6 +193,15 @@ func defaultBlobURL(projectName string, name string, digest string) string {
|
||||||
return fmt.Sprintf("/v2/%s/library/%s/blobs/%s", projectName, name, digest)
|
return fmt.Sprintf("/v2/%s/library/%s/blobs/%s", projectName, name, digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upstreamRegistryConnectionKey get upstream registry connection key
|
||||||
|
func upstreamRegistryConnectionKey(art lib.ArtifactInfo) string {
|
||||||
|
limitOnProject := os.Getenv(upstreamRegistryLimitOnProject)
|
||||||
|
if strings.EqualFold("true", limitOnProject) {
|
||||||
|
return fmt.Sprintf("{upstream_registry_connection}:%s", art.ProjectName)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{upstream_registry_connection}:%s:%s", art.Repository, art.Digest)
|
||||||
|
}
|
||||||
|
|
||||||
func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) error {
|
func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) error {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
art, p, proxyCtl, err := preCheck(ctx, true)
|
art, p, proxyCtl, err := preCheck(ctx, true)
|
||||||
|
@ -219,6 +248,20 @@ func handleManifest(w http.ResponseWriter, r *http.Request, next http.Handler) e
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if p.MaxUpstreamConnection() > 0 {
|
||||||
|
client, err := redis.GetHarborClient()
|
||||||
|
if err != nil {
|
||||||
|
return errors.NewErrs(err)
|
||||||
|
}
|
||||||
|
key := upstreamRegistryConnectionKey(art)
|
||||||
|
log.Debugf("handle manifest key %v", key)
|
||||||
|
if !connection.Limiter.Acquire(ctx, client, key, p.MaxUpstreamConnection()) {
|
||||||
|
log.Infof("current connection exceed max connections to upstream registry")
|
||||||
|
// send http code 429 to client
|
||||||
|
return tooManyRequestsError
|
||||||
|
}
|
||||||
|
defer connection.Limiter.Release(context.Background(), client, key) // use background context in defer to avoid been canceled
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("the tag is %v, digest is %v", art.Tag, art.Digest)
|
log.Debugf("the tag is %v, digest is %v", art.Tag, art.Digest)
|
||||||
if r.Method == http.MethodHead {
|
if r.Method == http.MethodHead {
|
||||||
|
|
|
@ -163,9 +163,10 @@ func (a *projectAPI) CreateProject(ctx context.Context, params operation.CreateP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore metadata.proxy_speed_kb for non-proxy-cache project
|
// ignore metadata.proxy_speed_kb and metadata.max_upstream_conn for non-proxy-cache project
|
||||||
if req.RegistryID == nil {
|
if req.RegistryID == nil {
|
||||||
req.Metadata.ProxySpeedKb = nil
|
req.Metadata.ProxySpeedKb = nil
|
||||||
|
req.Metadata.MaxUpstreamConn = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore enable_content_trust metadata for proxy cache project
|
// ignore enable_content_trust metadata for proxy cache project
|
||||||
|
@ -566,9 +567,10 @@ func (a *projectAPI) UpdateProject(ctx context.Context, params operation.UpdateP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore metadata.proxy_speed_kb for non-proxy-cache project
|
// ignore metadata.proxy_speed_kb and metadata.max_upstream_conn for non-proxy-cache project
|
||||||
if params.Project.Metadata != nil && !p.IsProxy() {
|
if params.Project.Metadata != nil && !p.IsProxy() {
|
||||||
params.Project.Metadata.ProxySpeedKb = nil
|
params.Project.Metadata.ProxySpeedKb = nil
|
||||||
|
params.Project.Metadata.MaxUpstreamConn = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore enable_content_trust metadata for proxy cache project
|
// ignore enable_content_trust metadata for proxy cache project
|
||||||
|
@ -818,6 +820,12 @@ func (a *projectAPI) validateProjectReq(ctx context.Context, req *models.Project
|
||||||
return errors.BadRequestError(nil).WithMessagef("metadata.proxy_speed_kb should by an int32, but got: '%s', err: %s", *ps, err)
|
return errors.BadRequestError(nil).WithMessagef("metadata.proxy_speed_kb should by an int32, but got: '%s', err: %s", *ps, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cnt := req.Metadata.MaxUpstreamConn; cnt != nil {
|
||||||
|
if _, err := strconv.ParseInt(*cnt, 10, 32); err != nil {
|
||||||
|
return errors.BadRequestError(nil).WithMessagef("metadata.max_upstream_conn should be an int, but got '%s', err: %s", *cnt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.StorageLimit != nil {
|
if req.StorageLimit != nil {
|
||||||
|
|
|
@ -161,6 +161,12 @@ func (p *projectMetadataAPI) validate(metas map[string]string) (map[string]strin
|
||||||
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid value: %s", value)
|
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid value: %s", value)
|
||||||
}
|
}
|
||||||
metas[proModels.ProMetaProxySpeed] = strconv.FormatInt(v, 10)
|
metas[proModels.ProMetaProxySpeed] = strconv.FormatInt(v, 10)
|
||||||
|
case proModels.ProMetaMaxUpstreamConn:
|
||||||
|
v, err := strconv.ParseInt(value, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid value: %s", value)
|
||||||
|
}
|
||||||
|
metas[proModels.ProMetaMaxUpstreamConn] = strconv.FormatInt(v, 10)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid key: %s", key)
|
return nil, errors.New(nil).WithCode(errors.BadRequestCode).WithMessagef("invalid key: %s", key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
// 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 handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
proModels "github.com/goharbor/harbor/src/pkg/project/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
api := &projectMetadataAPI{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
metas map[string]string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Invalid max upstream conn value",
|
||||||
|
metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "invalid"},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max upstream conn value 0",
|
||||||
|
metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "0"},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max upstream conn value -1",
|
||||||
|
metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "-1"},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normal max upstream conn value",
|
||||||
|
metas: map[string]string{proModels.ProMetaMaxUpstreamConn: "30"},
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unsupported key",
|
||||||
|
metas: map[string]string{"unsupported_key": "value"},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty map",
|
||||||
|
metas: map[string]string{},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := api.validate(tt.metas)
|
||||||
|
if tt.expectErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, result)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue