mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 05:28:07 +08:00
ba8575d960
Adds a configurable maximum repository count per registry instance. When maxRepos is set on StorageConfig, manifest pushes that would create a new repository beyond the limit are rejected with HTTP 429 TOOMANYREQUESTS. Pushes to existing repositories are always allowed. Implemented as an always-available feature in pkg/api (not a build-tag extension). MaxRepos is a field on StorageConfig, enabled when > 0. - repoQuotaMiddleware on the dist-spec router intercepts manifest PUTs. New-repo pushes are serialized with a sync.Mutex to prevent concurrent requests from exceeding the limit. - Adds CountRepos(ctx) to the MetaDB interface with efficient implementations: BoltDB (Stats().KeyN), Redis (HLen), DynamoDB (Scan with Select=COUNT). - Config.IsQuotaEnabled() added, wired into controller.go metaDB init. - Four integration tests (enforcement, concurrency, disabled, unconfigured) and backend-specific CountRepos tests for BoltDB, Redis, and DynamoDB. Signed-off-by: Bachir Khiati <bachir.khiati@gmail.com>
140 lines
3.6 KiB
Go
140 lines
3.6 KiB
Go
package api_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
"gopkg.in/resty.v1"
|
|
|
|
"zotregistry.dev/zot/v2/pkg/api"
|
|
"zotregistry.dev/zot/v2/pkg/api/config"
|
|
test "zotregistry.dev/zot/v2/pkg/test/common"
|
|
. "zotregistry.dev/zot/v2/pkg/test/image-utils"
|
|
)
|
|
|
|
func startQuotaServer(t *testing.T, maxRepos int) (string, func()) {
|
|
t.Helper()
|
|
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.Storage.RootDirectory = t.TempDir()
|
|
conf.Storage.MaxRepos = maxRepos
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctlrManager := test.NewControllerManager(ctlr)
|
|
ctlrManager.StartAndWait(port)
|
|
|
|
return test.GetBaseURL(port), func() { ctlrManager.StopServer() }
|
|
}
|
|
|
|
func TestQuotaEnforcement(t *testing.T) {
|
|
Convey("Given a registry with maxRepos set to 2", t, func() {
|
|
baseURL, stop := startQuotaServer(t, 2)
|
|
defer stop()
|
|
|
|
Convey("Push to two different repos succeeds", func() {
|
|
err := UploadImage(CreateRandomImage(), baseURL, "repo1", "v1")
|
|
So(err, ShouldBeNil)
|
|
|
|
err = UploadImage(CreateRandomImage(), baseURL, "repo2", "v1")
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("Push to a third new repo is rejected with 429", func() {
|
|
img := CreateRandomImage()
|
|
manifestBody, err := json.Marshal(img.Manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().
|
|
SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(manifestBody).
|
|
Put(baseURL + "/v2/repo3/manifests/v1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusTooManyRequests)
|
|
|
|
var body map[string]any
|
|
So(json.Unmarshal(resp.Body(), &body), ShouldBeNil)
|
|
errors, ok := body["errors"].([]any)
|
|
So(ok, ShouldBeTrue)
|
|
So(len(errors), ShouldBeGreaterThan, 0)
|
|
firstErr, ok := errors[0].(map[string]any)
|
|
So(ok, ShouldBeTrue)
|
|
So(firstErr["code"], ShouldEqual, "TOOMANYREQUESTS")
|
|
|
|
detail, ok := firstErr["detail"].(map[string]any)
|
|
So(ok, ShouldBeTrue)
|
|
So(detail["limit"], ShouldEqual, "2")
|
|
})
|
|
|
|
Convey("Push a new tag to an existing repo is allowed at the limit", func() {
|
|
err := UploadImage(CreateRandomImage(), baseURL, "repo1", "v2")
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("Re-pushing an existing tag is allowed at the limit", func() {
|
|
err := UploadImage(CreateRandomImage(), baseURL, "repo2", "v1")
|
|
So(err, ShouldBeNil)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestQuotaDisabled(t *testing.T) {
|
|
Convey("Given a registry with maxRepos set to 0 (disabled)", t, func() {
|
|
baseURL, stop := startQuotaServer(t, 0)
|
|
defer stop()
|
|
|
|
Convey("Pushing any number of repos succeeds", func() {
|
|
for _, repo := range []string{"repo1", "repo2", "repo3"} {
|
|
err := UploadImage(CreateRandomImage(), baseURL, repo, "v1")
|
|
So(err, ShouldBeNil)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestQuotaConcurrency(t *testing.T) {
|
|
Convey("Given a registry with maxRepos set to 5", t, func() {
|
|
baseURL, stop := startQuotaServer(t, 5)
|
|
defer stop()
|
|
|
|
Convey("Concurrent pushes to different new repos do not exceed the limit", func() {
|
|
const goroutines = 10
|
|
|
|
var wg sync.WaitGroup
|
|
results := make([]int, goroutines)
|
|
|
|
for i := range goroutines {
|
|
idx := i
|
|
wg.Go(func() {
|
|
err := UploadImage(CreateRandomImage(), baseURL, fmt.Sprintf("concurrent-repo-%d", idx), "v1")
|
|
if err != nil {
|
|
results[idx] = http.StatusTooManyRequests
|
|
} else {
|
|
results[idx] = http.StatusCreated
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
created := 0
|
|
rejected := 0
|
|
|
|
for _, code := range results {
|
|
if code == http.StatusCreated {
|
|
created++
|
|
} else {
|
|
rejected++
|
|
}
|
|
}
|
|
|
|
So(created, ShouldBeLessThanOrEqualTo, 5)
|
|
So(rejected, ShouldBeGreaterThanOrEqualTo, 5)
|
|
})
|
|
})
|
|
}
|