mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
feat(api): add repository quota enforcement middleware (#3923)
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>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
zerr "zotregistry.dev/zot/v2/errors"
|
||||
"zotregistry.dev/zot/v2/pkg/api/config"
|
||||
apiErr "zotregistry.dev/zot/v2/pkg/api/errors"
|
||||
zcommon "zotregistry.dev/zot/v2/pkg/common"
|
||||
"zotregistry.dev/zot/v2/pkg/log"
|
||||
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
|
||||
)
|
||||
|
||||
func repoQuotaMiddleware(maxRepos int, metaDB mTypes.MetaDB, log log.Logger) mux.MiddlewareFunc {
|
||||
var quotaMu sync.Mutex
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
// "reference" is only set on /v2/{name}/manifests/{reference} routes.
|
||||
if _, ok := vars["reference"]; !ok {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
repoName := vars["name"]
|
||||
if repoName == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err := metaDB.GetRepoMeta(r.Context(), repoName)
|
||||
if err == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !errors.Is(err, zerr.ErrRepoMetaNotFound) {
|
||||
log.Error().Err(err).Str("repo", repoName).
|
||||
Msg("failed to check repo existence for quota, allowing push")
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
quotaMu.Lock()
|
||||
defer quotaMu.Unlock()
|
||||
|
||||
// Re-check after acquiring the lock: another request may have
|
||||
// created this repo while we were waiting.
|
||||
_, err = metaDB.GetRepoMeta(r.Context(), repoName)
|
||||
if err == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
count, err := metaDB.CountRepos(r.Context())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to count repos for quota, allowing push")
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if count >= maxRepos {
|
||||
log.Warn().
|
||||
Str("repo", repoName).
|
||||
Int("current", count).
|
||||
Int("limit", maxRepos).
|
||||
Msg("repository quota limit reached, rejecting push")
|
||||
|
||||
detail := map[string]string{"limit": strconv.Itoa(maxRepos)}
|
||||
zcommon.WriteJSON(w, http.StatusTooManyRequests,
|
||||
apiErr.NewErrorList(apiErr.NewError(apiErr.TOOMANYREQUESTS).AddDetail(detail)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupQuotaMiddleware(
|
||||
conf *config.Config,
|
||||
router *mux.Router,
|
||||
metaDB mTypes.MetaDB,
|
||||
log log.Logger,
|
||||
) {
|
||||
if !conf.IsQuotaEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
if metaDB == nil {
|
||||
log.Warn().Msg("metaDB is not initialized, repository quota enforcement disabled")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Int("maxRepos", conf.Storage.MaxRepos).Msg("repository quota enforcement enabled")
|
||||
router.Use(repoQuotaMiddleware(conf.Storage.MaxRepos, metaDB, log))
|
||||
}
|
||||
Reference in New Issue
Block a user