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:
He Weiwei 2020-04-08 19:17:32 +00:00
parent 6ad855f0ee
commit 0b26b36737
4 changed files with 205 additions and 56 deletions

View File

@ -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 = &quotatesting.Manager{}
suite.ctl = &controller{quotaMgr: suite.quotaMgr, reservedExpiration: defaultReservedExpiration}
hardLimits := types.ResourceList{types.ResourceStorage: 100}
suite.quota = &quota.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 := &quotatesting.Manager{} mock.OnAnything(suite.quotaMgr, "GetByRefForUpdate").Return(suite.quota, nil)
hardLimits := types.ResourceList{types.ResourceStorage: 100}
mock.OnAnything(quotaMgr, "GetByRefForUpdate").Return(&quota.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 := &quotatesting.Manager{} mock.OnAnything(suite.quotaMgr, "GetByRefForUpdate").Return(suite.quota, nil)
hardLimits := types.ResourceList{types.ResourceStorage: 100}
mock.OnAnything(quotaMgr, "GetByRefForUpdate").Return(&quota.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) TestRefresh() {
suite.PrepareForUpdate(suite.quota, types.ResourceList{types.ResourceStorage: 0})
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
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() { func (suite *ControllerTestSuite) TestRequest() {
quotaMgr := &quotatesting.Manager{} suite.PrepareForUpdate(suite.quota, nil)
hardLimits := types.ResourceList{types.ResourceStorage: 100}
q := &quota.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()
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) {

View File

@ -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
} }

View File

@ -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)

View File

@ -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...)
} }