mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-18 22:57:38 +01:00
feat(quota): ignore limitation support for quota RefreshMiddleware
1. Ignore limitation when refresh quota for project. 2. Return 403 when quota errors occurred. 3. Add test for Refresh method of quota controller. Closes #11512 Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
parent
6ad855f0ee
commit
0b26b36737
@ -16,6 +16,7 @@ package quota
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -34,6 +35,34 @@ import (
|
|||||||
|
|
||||||
type ControllerTestSuite struct {
|
type ControllerTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
|
reference string
|
||||||
|
driver *drivertesting.Driver
|
||||||
|
quotaMgr *quotatesting.Manager
|
||||||
|
ctl Controller
|
||||||
|
|
||||||
|
quota *quota.Quota
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) SetupTest() {
|
||||||
|
suite.reference = "mock"
|
||||||
|
|
||||||
|
suite.driver = &drivertesting.Driver{}
|
||||||
|
driver.Register(suite.reference, suite.driver)
|
||||||
|
|
||||||
|
suite.quotaMgr = "atesting.Manager{}
|
||||||
|
suite.ctl = &controller{quotaMgr: suite.quotaMgr, reservedExpiration: defaultReservedExpiration}
|
||||||
|
|
||||||
|
hardLimits := types.ResourceList{types.ResourceStorage: 100}
|
||||||
|
suite.quota = "a.Quota{Hard: hardLimits.String(), Used: types.Zero(hardLimits).String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) PrepareForUpdate(q *quota.Quota, newUsage interface{}) {
|
||||||
|
mock.OnAnything(suite.quotaMgr, "GetByRefForUpdate").Return(q, nil)
|
||||||
|
|
||||||
|
mock.OnAnything(suite.driver, "CalculateUsage").Return(newUsage, nil)
|
||||||
|
|
||||||
|
mock.OnAnything(suite.quotaMgr, "Update").Return(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ControllerTestSuite) TestGetReservedResources() {
|
func (suite *ControllerTestSuite) TestGetReservedResources() {
|
||||||
@ -66,80 +95,113 @@ func (suite *ControllerTestSuite) TestGetReservedResources() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ControllerTestSuite) TestReserveResources() {
|
func (suite *ControllerTestSuite) TestReserveResources() {
|
||||||
quotaMgr := "atesting.Manager{}
|
mock.OnAnything(suite.quotaMgr, "GetByRefForUpdate").Return(suite.quota, nil)
|
||||||
|
|
||||||
hardLimits := types.ResourceList{types.ResourceStorage: 100}
|
|
||||||
|
|
||||||
mock.OnAnything(quotaMgr, "GetByRefForUpdate").Return("a.Quota{Hard: hardLimits.String(), Used: types.Zero(hardLimits).String()}, nil)
|
|
||||||
|
|
||||||
ctl := &controller{quotaMgr: quotaMgr, reservedExpiration: defaultReservedExpiration}
|
|
||||||
|
|
||||||
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
reference, referenceID := "reference", uuid.New().String()
|
referenceID := uuid.New().String()
|
||||||
resources := types.ResourceList{types.ResourceStorage: 100}
|
resources := types.ResourceList{types.ResourceStorage: 100}
|
||||||
|
|
||||||
suite.Nil(ctl.reserveResources(ctx, reference, referenceID, resources))
|
ctl := suite.ctl.(*controller)
|
||||||
|
|
||||||
suite.Error(ctl.reserveResources(ctx, reference, referenceID, resources))
|
suite.Nil(ctl.reserveResources(ctx, suite.reference, referenceID, resources))
|
||||||
|
|
||||||
|
suite.Error(ctl.reserveResources(ctx, suite.reference, referenceID, resources))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ControllerTestSuite) TestUnreserveResources() {
|
func (suite *ControllerTestSuite) TestUnreserveResources() {
|
||||||
quotaMgr := "atesting.Manager{}
|
mock.OnAnything(suite.quotaMgr, "GetByRefForUpdate").Return(suite.quota, nil)
|
||||||
|
|
||||||
hardLimits := types.ResourceList{types.ResourceStorage: 100}
|
|
||||||
|
|
||||||
mock.OnAnything(quotaMgr, "GetByRefForUpdate").Return("a.Quota{Hard: hardLimits.String(), Used: types.Zero(hardLimits).String()}, nil)
|
|
||||||
|
|
||||||
ctl := &controller{quotaMgr: quotaMgr, reservedExpiration: defaultReservedExpiration}
|
|
||||||
|
|
||||||
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
reference, referenceID := "reference", uuid.New().String()
|
referenceID := uuid.New().String()
|
||||||
resources := types.ResourceList{types.ResourceStorage: 100}
|
resources := types.ResourceList{types.ResourceStorage: 100}
|
||||||
|
|
||||||
suite.Nil(ctl.reserveResources(ctx, reference, referenceID, resources))
|
ctl := suite.ctl.(*controller)
|
||||||
|
|
||||||
suite.Error(ctl.reserveResources(ctx, reference, referenceID, resources))
|
suite.Nil(ctl.reserveResources(ctx, suite.reference, referenceID, resources))
|
||||||
|
|
||||||
suite.Nil(ctl.unreserveResources(ctx, reference, referenceID, resources))
|
suite.Error(ctl.reserveResources(ctx, suite.reference, referenceID, resources))
|
||||||
|
|
||||||
suite.Nil(ctl.reserveResources(ctx, reference, referenceID, resources))
|
suite.Nil(ctl.unreserveResources(ctx, suite.reference, referenceID, resources))
|
||||||
|
|
||||||
|
suite.Nil(ctl.reserveResources(ctx, suite.reference, referenceID, resources))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ControllerTestSuite) TestRequest() {
|
func (suite *ControllerTestSuite) TestRefresh() {
|
||||||
quotaMgr := "atesting.Manager{}
|
suite.PrepareForUpdate(suite.quota, types.ResourceList{types.ResourceStorage: 0})
|
||||||
|
|
||||||
hardLimits := types.ResourceList{types.ResourceStorage: 100}
|
|
||||||
|
|
||||||
q := "a.Quota{Hard: hardLimits.String(), Used: types.Zero(hardLimits).String()}
|
|
||||||
used := types.ResourceList{types.ResourceStorage: 0}
|
|
||||||
|
|
||||||
mock.OnAnything(quotaMgr, "GetByRefForUpdate").Return(q, nil)
|
|
||||||
|
|
||||||
mock.OnAnything(quotaMgr, "Update").Return(nil).Run(func(mock.Arguments) {
|
|
||||||
q.SetUsed(used)
|
|
||||||
})
|
|
||||||
|
|
||||||
d := &drivertesting.Driver{}
|
|
||||||
|
|
||||||
mock.OnAnything(d, "CalculateUsage").Return(used, nil).Run(func(args mock.Arguments) {
|
|
||||||
used[types.ResourceStorage]++
|
|
||||||
})
|
|
||||||
|
|
||||||
driver.Register("mock", d)
|
|
||||||
|
|
||||||
ctl := &controller{quotaMgr: quotaMgr, reservedExpiration: defaultReservedExpiration}
|
|
||||||
|
|
||||||
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
reference, referenceID := "mock", "1"
|
referenceID := uuid.New().String()
|
||||||
|
|
||||||
|
suite.Nil(suite.ctl.Refresh(ctx, suite.reference, referenceID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) TestRefreshDriverNotFound() {
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
|
||||||
|
suite.Error(suite.ctl.Refresh(ctx, uuid.New().String(), uuid.New().String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) TestRefershNegativeUsage() {
|
||||||
|
suite.PrepareForUpdate(suite.quota, types.ResourceList{types.ResourceStorage: -1})
|
||||||
|
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
referenceID := uuid.New().String()
|
||||||
|
|
||||||
|
suite.Error(suite.ctl.Refresh(ctx, suite.reference, referenceID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) TestRefreshUsageExceed() {
|
||||||
|
suite.PrepareForUpdate(suite.quota, types.ResourceList{types.ResourceStorage: 101})
|
||||||
|
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
referenceID := uuid.New().String()
|
||||||
|
|
||||||
|
suite.Error(suite.ctl.Refresh(ctx, suite.reference, referenceID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) TestRefreshIgnoreLimitation() {
|
||||||
|
suite.PrepareForUpdate(suite.quota, types.ResourceList{types.ResourceStorage: 101})
|
||||||
|
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
referenceID := uuid.New().String()
|
||||||
|
|
||||||
|
suite.Nil(suite.ctl.Refresh(ctx, suite.reference, referenceID, IgnoreLimitation(true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) TestNoResourcesRequest() {
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
referenceID := uuid.New().String()
|
||||||
|
|
||||||
|
suite.Nil(suite.ctl.Request(ctx, suite.reference, referenceID, nil, func() error { return nil }))
|
||||||
|
}
|
||||||
|
func (suite *ControllerTestSuite) TestRequest() {
|
||||||
|
suite.PrepareForUpdate(suite.quota, nil)
|
||||||
|
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
referenceID := uuid.New().String()
|
||||||
resources := types.ResourceList{types.ResourceStorage: 100}
|
resources := types.ResourceList{types.ResourceStorage: 100}
|
||||||
|
|
||||||
{
|
suite.Nil(suite.ctl.Request(ctx, suite.reference, referenceID, resources, func() error { return nil }))
|
||||||
suite.Nil(ctl.Request(ctx, reference, referenceID, resources, func() error { return nil }))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{
|
func (suite *ControllerTestSuite) TestRequestExceed() {
|
||||||
suite.Error(ctl.Request(ctx, reference, referenceID, resources, func() error { return nil }))
|
suite.PrepareForUpdate(suite.quota, nil)
|
||||||
}
|
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
referenceID := uuid.New().String()
|
||||||
|
resources := types.ResourceList{types.ResourceStorage: 101}
|
||||||
|
|
||||||
|
suite.Error(suite.ctl.Request(ctx, suite.reference, referenceID, resources, func() error { return nil }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ControllerTestSuite) TestRequestFunctionFailed() {
|
||||||
|
suite.PrepareForUpdate(suite.quota, nil)
|
||||||
|
|
||||||
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
referenceID := uuid.New().String()
|
||||||
|
resources := types.ResourceList{types.ResourceStorage: 100}
|
||||||
|
|
||||||
|
suite.Error(suite.ctl.Request(ctx, suite.reference, referenceID, resources, func() error { return fmt.Errorf("error") }))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestControllerTestSuite(t *testing.T) {
|
func TestControllerTestSuite(t *testing.T) {
|
||||||
|
@ -15,12 +15,13 @@
|
|||||||
package quota
|
package quota
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
cq "github.com/goharbor/harbor/src/controller/quota"
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/pkg/notification"
|
"github.com/goharbor/harbor/src/pkg/notification"
|
||||||
"github.com/goharbor/harbor/src/pkg/notifier/event"
|
"github.com/goharbor/harbor/src/pkg/notifier/event"
|
||||||
@ -167,14 +168,23 @@ func RequestMiddleware(config RequestConfig, skippers ...middleware.Skipper) fun
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.Reset()
|
res.Reset()
|
||||||
|
|
||||||
|
var errs quota.Errors
|
||||||
|
if errors.As(err, &errs) {
|
||||||
|
serror.SendError(res, errors.DeniedError(nil).WithMessage(errs.Error()))
|
||||||
|
} else {
|
||||||
serror.SendError(res, err)
|
serror.SendError(res, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}, skippers...)
|
}, skippers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshConfig refresh quota usage middleware config
|
// RefreshConfig refresh quota usage middleware config
|
||||||
type RefreshConfig struct {
|
type RefreshConfig struct {
|
||||||
|
// IgnoreLimitation allow quota usage exceed the limitation when it's true
|
||||||
|
IgnoreLimitation bool
|
||||||
|
|
||||||
// ReferenceObject returns reference object its quota usage will refresh by reference and reference id
|
// ReferenceObject returns reference object its quota usage will refresh by reference and reference id
|
||||||
ReferenceObject func(*http.Request) (reference string, referenceID string, err error)
|
ReferenceObject func(*http.Request) (reference string, referenceID string, err error)
|
||||||
}
|
}
|
||||||
@ -210,8 +220,14 @@ func RefreshMiddleware(config RefreshConfig, skipers ...middleware.Skipper) func
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = quotaController.Refresh(r.Context(), reference, referenceID); err != nil {
|
if err = quotaController.Refresh(r.Context(), reference, referenceID, cq.IgnoreLimitation(config.IgnoreLimitation)); err != nil {
|
||||||
logger.Errorf("refresh quota for %s %s failed, error: %v", reference, referenceID, err)
|
logger.Errorf("refresh quota for %s %s failed, error: %v", reference, referenceID, err)
|
||||||
|
|
||||||
|
var errs quota.Errors
|
||||||
|
if errors.As(err, &errs) {
|
||||||
|
return errors.DeniedError(nil).WithMessage(errs.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/controller/blob"
|
"github.com/goharbor/harbor/src/controller/blob"
|
||||||
"github.com/goharbor/harbor/src/controller/project"
|
"github.com/goharbor/harbor/src/controller/project"
|
||||||
"github.com/goharbor/harbor/src/controller/quota"
|
"github.com/goharbor/harbor/src/controller/quota"
|
||||||
|
pquota "github.com/goharbor/harbor/src/pkg/quota"
|
||||||
"github.com/goharbor/harbor/src/pkg/types"
|
"github.com/goharbor/harbor/src/pkg/types"
|
||||||
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||||
blobtesting "github.com/goharbor/harbor/src/testing/controller/blob"
|
blobtesting "github.com/goharbor/harbor/src/testing/controller/blob"
|
||||||
@ -193,6 +194,27 @@ func (suite *RequestMiddlewareTestSuite) TestResourcesRequestFailed() {
|
|||||||
suite.Equal(http.StatusInternalServerError, rr.Code)
|
suite.Equal(http.StatusInternalServerError, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *RequestMiddlewareTestSuite) TestResourcesRequestDenied() {
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/url", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
reference, referenceID := "project", "1"
|
||||||
|
resources := types.ResourceList{types.ResourceStorage: 100}
|
||||||
|
config := suite.makeRequestConfig(reference, referenceID, resources)
|
||||||
|
|
||||||
|
mock.OnAnything(suite.quotaController, "IsEnabled").Return(true, nil)
|
||||||
|
var errs pquota.Errors
|
||||||
|
errs = errs.Add(fmt.Errorf("Exceed"))
|
||||||
|
mock.OnAnything(suite.quotaController, "Request").Return(errs)
|
||||||
|
|
||||||
|
RequestMiddleware(config)(next).ServeHTTP(rr, req)
|
||||||
|
suite.Equal(http.StatusForbidden, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRequestMiddlewareTestSuite(t *testing.T) {
|
func TestRequestMiddlewareTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &RequestMiddlewareTestSuite{})
|
suite.Run(t, &RequestMiddlewareTestSuite{})
|
||||||
}
|
}
|
||||||
@ -231,6 +253,54 @@ func (suite *RefreshMiddlewareTestSuite) TestQuotaDisabled() {
|
|||||||
suite.Equal(http.StatusOK, rr.Code)
|
suite.Equal(http.StatusOK, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *RefreshMiddlewareTestSuite) TestQuotaIsEnabledFailed() {
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/url", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
reference, referenceID := "project", "1"
|
||||||
|
|
||||||
|
config := RefreshConfig{
|
||||||
|
ReferenceObject: func(*http.Request) (string, string, error) {
|
||||||
|
return reference, referenceID, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mock.OnAnything(suite.quotaController, "IsEnabled").Return(false, fmt.Errorf("error"))
|
||||||
|
|
||||||
|
RefreshMiddleware(config)(next).ServeHTTP(rr, req)
|
||||||
|
suite.Equal(http.StatusInternalServerError, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RefreshMiddlewareTestSuite) TestInvalidConfig() {
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/url", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config := RefreshConfig{}
|
||||||
|
RefreshMiddleware(config)(next).ServeHTTP(rr, req)
|
||||||
|
suite.Equal(http.StatusInternalServerError, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *RefreshMiddlewareTestSuite) TestNotSuccess() {
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/url", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
config := RefreshConfig{}
|
||||||
|
RefreshMiddleware(config)(next).ServeHTTP(rr, req)
|
||||||
|
suite.Equal(http.StatusBadRequest, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *RefreshMiddlewareTestSuite) TestRefershOK() {
|
func (suite *RefreshMiddlewareTestSuite) TestRefershOK() {
|
||||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
// RefreshForProjectMiddleware middleware which refresh the quota usage of project after the response success
|
// RefreshForProjectMiddleware middleware which refresh the quota usage of project after the response success
|
||||||
func RefreshForProjectMiddleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler {
|
func RefreshForProjectMiddleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler {
|
||||||
return RefreshMiddleware(RefreshConfig{
|
return RefreshMiddleware(RefreshConfig{
|
||||||
|
IgnoreLimitation: true,
|
||||||
ReferenceObject: projectReferenceObject,
|
ReferenceObject: projectReferenceObject,
|
||||||
}, skippers...)
|
}, skippers...)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user