Unified storage: Add quotas app to apiserver (#114425)
* initial generation * went through doc to add new resource * added dummy kind so grafana will run * added dummy handler and custom route * fix app name * gets custom route working - still a dummy route * adds groupOverride to manifest * adds quotas to grpc client and server * WIP - trying to get api recognized - not working * Gets route working * fixes group and resource vars * expects group and resource as separate params * set content-type header on response * removes Quotas kind and regens * Update grafana-app-sdk to v0.48.5 * Update codegen * updates manifest * formatting * updates grafana-app-sdk version to 0.48.5 * regen ResourceClient mocks * adds tests * remove commented code * uncomment go mod tidy * fix tests and make update workspace * adds quotas app to codeowners * formatting * make gen-apps * deletes temp file * fix generated folder code * make gofmt * make gen-go * make update-workspace * add COPY apps/quotas to Dockerfile * fix test mock * fixes undefined NewFolderStatus() * make gen-apps, and add func for NewFolderStatus * make gen-apps again * make update-workspace * regen folder_object_gen.go * gofmt * fix linting * apps/folder make update-workspace * make gen-apps * make gen-apps * fixes enterprise_imports.go * go get testcontainers * adds feature toggle * make update-workspace * fix go mod * fix another client mock --------- Co-authored-by: Steve Simpson <steve@grafana.com>
This commit is contained in:
co-authored by
Steve Simpson
parent
297e886e1b
commit
a3daf0e39d
@@ -39,6 +39,7 @@ type ResourceClient interface {
|
||||
resourcepb.BulkStoreClient
|
||||
resourcepb.BlobStoreClient
|
||||
resourcepb.DiagnosticsClient
|
||||
resourcepb.QuotasClient
|
||||
}
|
||||
|
||||
// Internal implementation
|
||||
@@ -49,6 +50,7 @@ type resourceClient struct {
|
||||
resourcepb.BulkStoreClient
|
||||
resourcepb.BlobStoreClient
|
||||
resourcepb.DiagnosticsClient
|
||||
resourcepb.QuotasClient
|
||||
}
|
||||
|
||||
func NewResourceClient(conn, indexConn grpc.ClientConnInterface, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer trace.Tracer) (ResourceClient, error) {
|
||||
@@ -76,6 +78,7 @@ func newResourceClient(storageCc grpc.ClientConnInterface, indexCc grpc.ClientCo
|
||||
BulkStoreClient: resourcepb.NewBulkStoreClient(storageCc),
|
||||
BlobStoreClient: resourcepb.NewBlobStoreClient(storageCc),
|
||||
DiagnosticsClient: resourcepb.NewDiagnosticsClient(storageCc),
|
||||
QuotasClient: resourcepb.NewQuotasClient(storageCc),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +105,7 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient {
|
||||
&resourcepb.BlobStore_ServiceDesc,
|
||||
&resourcepb.BulkStore_ServiceDesc,
|
||||
&resourcepb.Diagnostics_ServiceDesc,
|
||||
&resourcepb.Quotas_ServiceDesc,
|
||||
} {
|
||||
channel.RegisterService(
|
||||
grpchan.InterceptServer(
|
||||
|
||||
@@ -394,6 +394,80 @@ func (_c *MockResourceClient_GetBlob_Call) RunAndReturn(run func(context.Context
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetQuotaUsage provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *MockResourceClient) GetQuotaUsage(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
for _i := range opts {
|
||||
_va[_i] = opts[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, in)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetQuotaUsage")
|
||||
}
|
||||
|
||||
var r0 *resourcepb.QuotaUsageResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) *resourcepb.QuotaUsageResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*resourcepb.QuotaUsageResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockResourceClient_GetQuotaUsage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetQuotaUsage'
|
||||
type MockResourceClient_GetQuotaUsage_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetQuotaUsage is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *resourcepb.QuotaUsageRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *MockResourceClient_Expecter) GetQuotaUsage(ctx interface{}, in interface{}, opts ...interface{}) *MockResourceClient_GetQuotaUsage_Call {
|
||||
return &MockResourceClient_GetQuotaUsage_Call{Call: _e.mock.On("GetQuotaUsage",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockResourceClient_GetQuotaUsage_Call) Run(run func(ctx context.Context, in *resourcepb.QuotaUsageRequest, opts ...grpc.CallOption)) *MockResourceClient_GetQuotaUsage_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*resourcepb.QuotaUsageRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockResourceClient_GetQuotaUsage_Call) Return(_a0 *resourcepb.QuotaUsageResponse, _a1 error) *MockResourceClient_GetQuotaUsage_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockResourceClient_GetQuotaUsage_Call) RunAndReturn(run func(context.Context, *resourcepb.QuotaUsageRequest, ...grpc.CallOption) (*resourcepb.QuotaUsageResponse, error)) *MockResourceClient_GetQuotaUsage_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetStats provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *MockResourceClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
|
||||
@@ -41,6 +41,7 @@ type ResourceServer interface {
|
||||
resourcepb.ManagedObjectIndexServer
|
||||
resourcepb.BlobStoreServer
|
||||
resourcepb.DiagnosticsServer
|
||||
resourcepb.QuotasServer
|
||||
}
|
||||
|
||||
type ListIterator interface {
|
||||
@@ -1466,6 +1467,38 @@ func (s *server) PutBlob(ctx context.Context, req *resourcepb.PutBlobRequest) (*
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func (s *server) GetQuotaUsage(ctx context.Context, req *resourcepb.QuotaUsageRequest) (*resourcepb.QuotaUsageResponse, error) {
|
||||
if s.overridesService == nil {
|
||||
return &resourcepb.QuotaUsageResponse{Error: &resourcepb.ErrorResult{
|
||||
Message: "overrides service not configured on resource server",
|
||||
Code: http.StatusNotImplemented,
|
||||
}}, nil
|
||||
}
|
||||
nsr := NamespacedResource{
|
||||
Namespace: req.Key.Namespace,
|
||||
Group: req.Key.Group,
|
||||
Resource: req.Key.Resource,
|
||||
}
|
||||
usage, err := s.backend.GetResourceStats(ctx, nsr, 0)
|
||||
if err != nil {
|
||||
return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil
|
||||
}
|
||||
limit, err := s.overridesService.GetQuota(ctx, nsr)
|
||||
if err != nil {
|
||||
return &resourcepb.QuotaUsageResponse{Error: AsErrorResult(err)}, nil
|
||||
}
|
||||
|
||||
// handle case where no resources exist yet - very unlikely but possible
|
||||
rsp := &resourcepb.QuotaUsageResponse{Limit: int64(limit.Limit)}
|
||||
if len(usage) <= 0 {
|
||||
rsp.Usage = 0
|
||||
} else {
|
||||
rsp.Usage = usage[0].Count
|
||||
}
|
||||
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func (s *server) getPartialObject(ctx context.Context, key *resourcepb.ResourceKey, rv int64) (utils.GrafanaMetaAccessor, *resourcepb.ErrorResult) {
|
||||
if r := verifyRequestKey(key); r != nil {
|
||||
return nil, r
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -22,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
"github.com/grafana/grafana/pkg/util/scheduler"
|
||||
)
|
||||
@@ -614,3 +617,70 @@ func TestArtificialDelayAfterSuccessfulOperation(t *testing.T) {
|
||||
check(t, false, &resourcepb.UpdateResponse{Error: AsErrorResult(errors.New("some error"))}, nil)
|
||||
check(t, false, &resourcepb.DeleteResponse{Error: AsErrorResult(errors.New("some error"))}, nil)
|
||||
}
|
||||
|
||||
func TestGetQuotaUsage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns error when overrides service is not configured", func(t *testing.T) {
|
||||
s := &server{
|
||||
overridesService: nil,
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Namespace: "stacks-123",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp.Error)
|
||||
assert.Equal(t, int32(http.StatusNotImplemented), resp.Error.Code)
|
||||
assert.Equal(t, "overrides service not configured on resource server", resp.Error.Message)
|
||||
})
|
||||
|
||||
t.Run("returns usage and limit successfully", func(t *testing.T) {
|
||||
// Create a temporary overrides config file
|
||||
tmpFile := filepath.Join(t.TempDir(), "overrides.yaml")
|
||||
content := `"123":
|
||||
quotas:
|
||||
dashboard.grafana.app/dashboards:
|
||||
limit: 500
|
||||
`
|
||||
require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644))
|
||||
|
||||
// Create a real OverridesService with the temp file
|
||||
overridesService, err := NewOverridesService(ctx, log.NewNopLogger(), prometheus.NewRegistry(), tracing.NewNoopTracerService(), ReloadOptions{
|
||||
FilePath: tmpFile,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, overridesService.init(ctx))
|
||||
defer func() {
|
||||
_ = overridesService.stop(ctx)
|
||||
}()
|
||||
|
||||
// Create a mock backend that returns resource stats (reusing mockStorageBackend from search_test.go)
|
||||
mockBackend := &mockStorageBackend{
|
||||
resourceStats: []ResourceStats{{Count: 42}},
|
||||
}
|
||||
|
||||
s := &server{
|
||||
backend: mockBackend,
|
||||
overridesService: overridesService,
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
resp, err := s.GetQuotaUsage(ctx, &resourcepb.QuotaUsageRequest{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Namespace: "stacks-123",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, resp.Error)
|
||||
assert.Equal(t, int64(42), resp.Usage)
|
||||
assert.Equal(t, int64(500), resp.Limit)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user