Merge remote-tracking branch 'origin' into axelav/dash-validator-app-mvp
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
_ "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
|
||||
_ "github.com/Azure/go-autorest/autorest"
|
||||
_ "github.com/Azure/go-autorest/autorest/adal"
|
||||
_ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
_ "github.com/beevik/etree"
|
||||
_ "github.com/blugelabs/bluge"
|
||||
_ "github.com/blugelabs/bluge_segment_api"
|
||||
|
||||
@@ -42,7 +42,6 @@ func newIAMAuthorizer(
|
||||
|
||||
// Identity specific resources
|
||||
legacyAuthorizer := gfauthorizer.NewResourceAuthorizer(legacyAccessClient)
|
||||
resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = legacyAuthorizer
|
||||
resourceAuthorizer["display"] = legacyAuthorizer
|
||||
|
||||
// Access specific resources
|
||||
@@ -55,6 +54,7 @@ func newIAMAuthorizer(
|
||||
resourceAuthorizer[iamv0.UserResourceInfo.GetName()] = authorizer
|
||||
resourceAuthorizer[iamv0.ExternalGroupMappingResourceInfo.GetName()] = allowAuthorizer
|
||||
resourceAuthorizer[iamv0.TeamResourceInfo.GetName()] = authorizer
|
||||
resourceAuthorizer[iamv0.TeamBindingResourceInfo.GetName()] = allowAuthorizer
|
||||
resourceAuthorizer["searchUsers"] = serviceAuthorizer
|
||||
resourceAuthorizer["searchTeams"] = serviceAuthorizer
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package authorizer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer/storewrapper"
|
||||
)
|
||||
|
||||
type TeamBindingAuthorizer struct {
|
||||
accessClient types.AccessClient
|
||||
}
|
||||
|
||||
var _ storewrapper.ResourceStorageAuthorizer = (*TeamBindingAuthorizer)(nil)
|
||||
|
||||
func NewTeamBindingAuthorizer(
|
||||
accessClient types.AccessClient,
|
||||
) *TeamBindingAuthorizer {
|
||||
return &TeamBindingAuthorizer{
|
||||
accessClient: accessClient,
|
||||
}
|
||||
}
|
||||
|
||||
// AfterGet implements ResourceStorageAuthorizer.
|
||||
func (r *TeamBindingAuthorizer) AfterGet(ctx context.Context, obj runtime.Object) error {
|
||||
authInfo, ok := types.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return storewrapper.ErrUnauthenticated
|
||||
}
|
||||
|
||||
concreteObj, ok := obj.(*iamv0.TeamBinding)
|
||||
if !ok {
|
||||
return apierrors.NewInternalError(fmt.Errorf("expected TeamBinding, got %T: %w", obj, storewrapper.ErrUnexpectedType))
|
||||
}
|
||||
|
||||
// Accesscontrol should check on the TeamResourceInfo group resource if the user can use VerbGetPermissions
|
||||
// on the team (TeamRef.Name) (handled below) OR if the subject's name (TeamBindingSpec.Subject.Name) is equal to the current Identity's UID/Identifier.
|
||||
if concreteObj.Spec.Subject.Name == authInfo.GetIdentifier() {
|
||||
return nil
|
||||
}
|
||||
|
||||
teamName := concreteObj.Spec.TeamRef.Name
|
||||
checkReq := types.CheckRequest{
|
||||
Namespace: authInfo.GetNamespace(),
|
||||
Group: iamv0.TeamResourceInfo.GroupResource().Group,
|
||||
Resource: iamv0.TeamResourceInfo.GroupResource().Resource,
|
||||
Verb: utils.VerbGetPermissions,
|
||||
Name: teamName,
|
||||
}
|
||||
res, err := r.accessClient.Check(ctx, authInfo, checkReq, "")
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
|
||||
if !res.Allowed {
|
||||
return apierrors.NewForbidden(
|
||||
iamv0.TeamBindingResourceInfo.GroupResource(),
|
||||
concreteObj.Name,
|
||||
fmt.Errorf("user cannot access team %s", teamName),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeCreate implements ResourceStorageAuthorizer.
|
||||
func (r *TeamBindingAuthorizer) BeforeCreate(ctx context.Context, obj runtime.Object) error {
|
||||
return r.beforeWrite(ctx, obj)
|
||||
}
|
||||
|
||||
// BeforeDelete implements ResourceStorageAuthorizer.
|
||||
func (r *TeamBindingAuthorizer) BeforeDelete(ctx context.Context, obj runtime.Object) error {
|
||||
return r.beforeWrite(ctx, obj)
|
||||
}
|
||||
|
||||
// BeforeUpdate implements ResourceStorageAuthorizer.
|
||||
func (r *TeamBindingAuthorizer) BeforeUpdate(ctx context.Context, obj runtime.Object) error {
|
||||
return r.beforeWrite(ctx, obj)
|
||||
}
|
||||
|
||||
func (r *TeamBindingAuthorizer) beforeWrite(ctx context.Context, obj runtime.Object) error {
|
||||
authInfo, ok := types.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return storewrapper.ErrUnauthenticated
|
||||
}
|
||||
|
||||
concreteObj, ok := obj.(*iamv0.TeamBinding)
|
||||
if !ok {
|
||||
return apierrors.NewInternalError(fmt.Errorf("expected TeamBinding, got %T: %w", obj, storewrapper.ErrUnexpectedType))
|
||||
}
|
||||
|
||||
teamName := concreteObj.Spec.TeamRef.Name
|
||||
checkReq := types.CheckRequest{
|
||||
Namespace: authInfo.GetNamespace(),
|
||||
Group: iamv0.GROUP,
|
||||
Resource: iamv0.TeamResourceInfo.GetName(),
|
||||
Verb: utils.VerbSetPermissions,
|
||||
Name: teamName,
|
||||
}
|
||||
|
||||
res, err := r.accessClient.Check(ctx, authInfo, checkReq, "")
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
|
||||
if !res.Allowed {
|
||||
return apierrors.NewForbidden(
|
||||
iamv0.TeamBindingResourceInfo.GroupResource(),
|
||||
concreteObj.Name,
|
||||
fmt.Errorf("user cannot write team %s", teamName),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterList implements ResourceStorageAuthorizer.
|
||||
func (r *TeamBindingAuthorizer) FilterList(ctx context.Context, list runtime.Object) (runtime.Object, error) {
|
||||
authInfo, ok := types.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, storewrapper.ErrUnauthenticated
|
||||
}
|
||||
|
||||
l, ok := list.(*iamv0.TeamBindingList)
|
||||
if !ok {
|
||||
return nil, apierrors.NewInternalError(fmt.Errorf("expected TeamBindingList, got %T: %w", list, storewrapper.ErrUnexpectedType))
|
||||
}
|
||||
|
||||
var filteredItems []iamv0.TeamBinding
|
||||
|
||||
listReq := types.ListRequest{
|
||||
Namespace: authInfo.GetNamespace(),
|
||||
Group: iamv0.TeamResourceInfo.GroupResource().Group,
|
||||
Resource: iamv0.TeamResourceInfo.GroupResource().Resource,
|
||||
Verb: utils.VerbGetPermissions,
|
||||
}
|
||||
canView, _, err := r.accessClient.Compile(ctx, authInfo, listReq)
|
||||
if err != nil {
|
||||
return nil, apierrors.NewInternalError(err)
|
||||
}
|
||||
|
||||
for _, item := range l.Items {
|
||||
// Accesscontrol should check on the TeamResourceInfo group resource if the user can use VerbGetPermissions
|
||||
// on the team (TeamRef.Name) OR if the subject's name (TeamBindingSpec.Subject.Name) is equal to the current Identity's UID/Identifier.
|
||||
if item.Spec.Subject.Name == authInfo.GetIdentifier() || canView(item.Spec.TeamRef.Name, "") {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
|
||||
l.Items = filteredItems
|
||||
return l, nil
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package authorizer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/authlib/types"
|
||||
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
|
||||
func newTeamBinding(teamName, name, subjectName string) *iamv0.TeamBinding {
|
||||
return &iamv0.TeamBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "org-2", Name: name},
|
||||
Spec: iamv0.TeamBindingSpec{
|
||||
TeamRef: iamv0.TeamBindingTeamRef{
|
||||
Name: teamName,
|
||||
},
|
||||
Subject: iamv0.TeamBindingspecSubject{
|
||||
Name: subjectName,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamBinding_AfterGet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
teamBinding *iamv0.TeamBinding
|
||||
shouldAllow bool
|
||||
checkCalled bool
|
||||
}{
|
||||
{
|
||||
name: "allow access via permission",
|
||||
teamBinding: newTeamBinding("team-1", "binding-1", "other"),
|
||||
shouldAllow: true,
|
||||
checkCalled: true,
|
||||
},
|
||||
{
|
||||
name: "deny access",
|
||||
teamBinding: newTeamBinding("team-1", "binding-1", "other"),
|
||||
shouldAllow: false,
|
||||
checkCalled: true, // called but returns allowed=false
|
||||
},
|
||||
{
|
||||
name: "allow access via subject match",
|
||||
teamBinding: newTeamBinding("team-1", "binding-1", "u001"),
|
||||
shouldAllow: true,
|
||||
checkCalled: false, // short-circuits
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
|
||||
require.NotNil(t, id)
|
||||
require.Equal(t, "u001", id.GetIdentifier())
|
||||
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(t, iamv0.GROUP, req.Group)
|
||||
require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource)
|
||||
require.Equal(t, "team-1", req.Name)
|
||||
require.Equal(t, utils.VerbGetPermissions, req.Verb)
|
||||
|
||||
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{checkFunc: checkFunc}
|
||||
authz := NewTeamBindingAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
err := authz.AfterGet(ctx, tt.teamBinding)
|
||||
if tt.shouldAllow {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
require.Equal(t, tt.checkCalled, accessClient.checkCalled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamBinding_FilterList(t *testing.T) {
|
||||
list := &iamv0.TeamBindingList{
|
||||
Items: []iamv0.TeamBinding{
|
||||
*newTeamBinding("team-1", "binding-1", "other"), // Access via permission
|
||||
*newTeamBinding("team-2", "binding-2", "other"), // No access
|
||||
*newTeamBinding("team-3", "binding-3", "u001"), // Access via subject match
|
||||
},
|
||||
}
|
||||
|
||||
compileFunc := func(id types.AuthInfo, req types.ListRequest) (types.ItemChecker, types.Zookie, error) {
|
||||
require.NotNil(t, id)
|
||||
require.Equal(t, "u001", id.GetIdentifier())
|
||||
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(t, iamv0.GROUP, req.Group)
|
||||
require.Equal(t, iamv0.TeamResourceInfo.GroupResource().Resource, req.Resource)
|
||||
require.Equal(t, utils.VerbGetPermissions, req.Verb)
|
||||
|
||||
return func(name, folder string) bool {
|
||||
return name == "team-1"
|
||||
}, &types.NoopZookie{}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{compileFunc: compileFunc}
|
||||
authz := NewTeamBindingAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
obj, err := authz.FilterList(ctx, list)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, list)
|
||||
require.True(t, accessClient.compileCalled)
|
||||
|
||||
filtered, ok := obj.(*iamv0.TeamBindingList)
|
||||
require.True(t, ok)
|
||||
require.Len(t, filtered.Items, 2)
|
||||
|
||||
names := []string{filtered.Items[0].Name, filtered.Items[1].Name}
|
||||
require.Contains(t, names, "binding-1")
|
||||
require.Contains(t, names, "binding-3")
|
||||
}
|
||||
|
||||
func TestTeamBinding_BeforeCreate(t *testing.T) {
|
||||
binding := newTeamBinding("team-1", "binding-1", "other")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shouldAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow create",
|
||||
shouldAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny create",
|
||||
shouldAllow: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(t, iamv0.GROUP, req.Group)
|
||||
require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource)
|
||||
require.Equal(t, "team-1", req.Name)
|
||||
require.Equal(t, utils.VerbSetPermissions, req.Verb)
|
||||
|
||||
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{checkFunc: checkFunc}
|
||||
authz := NewTeamBindingAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
err := authz.BeforeCreate(ctx, binding)
|
||||
if tt.shouldAllow {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
require.True(t, accessClient.checkCalled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamBinding_BeforeUpdate(t *testing.T) {
|
||||
binding := newTeamBinding("team-1", "binding-1", "other")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shouldAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow update",
|
||||
shouldAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny update",
|
||||
shouldAllow: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(t, iamv0.GROUP, req.Group)
|
||||
require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource)
|
||||
require.Equal(t, "team-1", req.Name)
|
||||
require.Equal(t, utils.VerbSetPermissions, req.Verb)
|
||||
|
||||
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{checkFunc: checkFunc}
|
||||
authz := NewTeamBindingAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
err := authz.BeforeUpdate(ctx, binding)
|
||||
if tt.shouldAllow {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
require.True(t, accessClient.checkCalled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTeamBinding_BeforeDelete(t *testing.T) {
|
||||
binding := newTeamBinding("team-1", "binding-1", "other")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shouldAllow bool
|
||||
}{
|
||||
{
|
||||
name: "allow delete",
|
||||
shouldAllow: true,
|
||||
},
|
||||
{
|
||||
name: "deny delete",
|
||||
shouldAllow: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
checkFunc := func(id types.AuthInfo, req *types.CheckRequest, folder string) (types.CheckResponse, error) {
|
||||
require.Equal(t, "org-2", req.Namespace)
|
||||
require.Equal(t, iamv0.GROUP, req.Group)
|
||||
require.Equal(t, iamv0.TeamResourceInfo.GetName(), req.Resource)
|
||||
require.Equal(t, "team-1", req.Name)
|
||||
require.Equal(t, utils.VerbSetPermissions, req.Verb)
|
||||
|
||||
return types.CheckResponse{Allowed: tt.shouldAllow}, nil
|
||||
}
|
||||
|
||||
accessClient := &fakeAccessClient{checkFunc: checkFunc}
|
||||
authz := NewTeamBindingAuthorizer(accessClient)
|
||||
ctx := types.WithAuthInfo(context.Background(), user)
|
||||
|
||||
err := authz.BeforeDelete(ctx, binding)
|
||||
if tt.shouldAllow {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
require.True(t, accessClient.checkCalled)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -361,7 +361,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts bui
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storage[teamBindingResource.StoragePath()] = teamBindingUniStore
|
||||
var teamBindingStore storewrapper.K8sStorage = teamBindingUniStore
|
||||
|
||||
// Only teamBindingStore exposes the AfterCreate, AfterDelete, and BeginUpdate hooks
|
||||
if enableZanzanaSync {
|
||||
@@ -376,8 +376,16 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateTeamBindingsAPIGroup(opts bui
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storage[teamBindingResource.StoragePath()] = dw
|
||||
|
||||
var ok bool
|
||||
teamBindingStore, ok = dw.(storewrapper.K8sStorage)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected storewrapper.K8sStorage, got %T", dw)
|
||||
}
|
||||
}
|
||||
|
||||
authzWrapper := storewrapper.New(teamBindingStore, iamauthorizer.NewTeamBindingAuthorizer(b.accessClient))
|
||||
storage[teamBindingResource.StoragePath()] = authzWrapper
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/dskit/flagext"
|
||||
@@ -15,11 +16,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
@@ -111,14 +116,25 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
|
||||
Help: "Time spent executing requests to resource server.",
|
||||
Buckets: prometheus.ExponentialBuckets(0.008, 4, 7),
|
||||
}, []string{"operation", "status_code"})
|
||||
factoryRequestRetries := promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "resource_server_client_request_retries_total",
|
||||
Help: "Total number of retries for requests to the resource server.",
|
||||
}, []string{"operation"})
|
||||
|
||||
factory := ringclient.PoolInstFunc(func(inst ring.InstanceDesc) (ringclient.PoolClient, error) {
|
||||
unaryInterceptors, streamInterceptors := grpcclient.Instrument(factoryRequestDuration)
|
||||
|
||||
// Add retry interceptors for transient connection issues
|
||||
unaryInterceptors = append(unaryInterceptors, ringClientRetryInterceptor())
|
||||
unaryInterceptors = append(unaryInterceptors, ringClientRetryInstrument(factoryRequestRetries))
|
||||
|
||||
opts, err := clientCfg.DialOption(unaryInterceptors, streamInterceptors, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = append(opts, connectionBackoffOptions())
|
||||
|
||||
conn, err := grpc.NewClient(inst.Addr, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial resource server %s %s: %s", inst.Id, inst.Addr, err)
|
||||
@@ -135,3 +151,40 @@ func newClientPool(clientCfg grpcclient.Config, log log.Logger, reg prometheus.R
|
||||
|
||||
return ringclient.NewPool(resource.RingName, poolCfg, nil, factory, clientsCount, log)
|
||||
}
|
||||
|
||||
// ringClientRetryInterceptor creates an interceptor to perform retries for unary methods.
|
||||
// It retries on ResourceExhausted and Unavailable codes, which are typical for
|
||||
// transient connection issues and rate limiting.
|
||||
func ringClientRetryInterceptor() grpc.UnaryClientInterceptor {
|
||||
return grpc_retry.UnaryClientInterceptor(
|
||||
grpc_retry.WithMax(3),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.1)),
|
||||
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
|
||||
)
|
||||
}
|
||||
|
||||
// ringClientRetryInstrument creates an interceptor to count retry attempts for metrics.
|
||||
func ringClientRetryInstrument(metric *prometheus.CounterVec) grpc.UnaryClientInterceptor {
|
||||
return func(ctx context.Context, method string, req, resp interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
// We can tell if a call is a retry by checking the retry attempt metadata.
|
||||
attempt, err := strconv.Atoi(metautils.ExtractOutgoing(ctx).Get(grpc_retry.AttemptMetadataKey))
|
||||
if err == nil && attempt > 0 {
|
||||
metric.WithLabelValues(method).Inc()
|
||||
}
|
||||
return invoker(ctx, method, req, resp, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// connectionBackoffOptions configures connection backoff parameters for faster recovery from
|
||||
// transient connection failures (e.g., during pod restarts).
|
||||
func connectionBackoffOptions() grpc.DialOption {
|
||||
return grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,11 +11,16 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
|
||||
|
||||
apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
secret "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
@@ -232,19 +237,16 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
|
||||
if o.StorageType != StorageTypeUnifiedGrpc {
|
||||
return nil
|
||||
}
|
||||
conn, err := grpc.NewClient(o.Address,
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
|
||||
grpcOpts := o.buildGrpcDialOptions()
|
||||
|
||||
conn, err := grpc.NewClient(o.Address, grpcOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var indexConn *grpc.ClientConn
|
||||
if o.SearchServerAddress != "" {
|
||||
indexConn, err = grpc.NewClient(o.SearchServerAddress,
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
indexConn, err = grpc.NewClient(o.SearchServerAddress, grpcOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -293,3 +295,42 @@ func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfi
|
||||
serverConfig.RESTOptionsGetter = getter
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildGrpcDialOptions creates gRPC dial options with resilience mechanisms:
|
||||
// - Round-robin load balancing with client-side health checking
|
||||
// - Retry interceptor for transient connection issues
|
||||
// - Keepalive for long-lived connections
|
||||
func (o *StorageOptions) buildGrpcDialOptions() []grpc.DialOption {
|
||||
// Retry interceptor for transient connection issues (codes.Unavailable includes connection refused)
|
||||
retryInterceptor := grpc_retry.UnaryClientInterceptor(
|
||||
grpc_retry.WithMax(3),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(time.Second, 0.5)),
|
||||
grpc_retry.WithCodes(codes.ResourceExhausted, codes.Unavailable),
|
||||
)
|
||||
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithChainUnaryInterceptor(retryInterceptor),
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
}),
|
||||
}
|
||||
|
||||
if o.GrpcClientKeepaliveTime > 0 {
|
||||
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: o.GrpcClientKeepaliveTime,
|
||||
Timeout: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}))
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -1073,13 +1073,6 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: identityAccessTeam,
|
||||
},
|
||||
{
|
||||
Name: "unifiedStorageSearchSprinkles",
|
||||
Description: "Enable sprinkles on unified storage search",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "managedDualWriter",
|
||||
Description: "Pick the dual write mode from database configs",
|
||||
|
||||
@@ -409,7 +409,6 @@ lokiLabelNamesQueryApi,2024-12-13T14:31:41Z,,5ac7443fcec0db412d3333044a82c2c26b5
|
||||
kubernetesCliDashboards,2024-12-13T22:55:43Z,2025-02-18T23:11:26Z,8f6e9f8ed0a5024a510cc337c9f1e6972bfb23d4,Stephanie Hingtgen
|
||||
useV2DashboardsAPI,2024-12-17T21:17:09Z,2025-03-12T17:43:32Z,070f0e4457c5967102ef157197073dc2662f6fb8,Dominik Prokop
|
||||
investigationsBackend,2024-12-18T08:31:03Z,,f46c07aba7b6faccd2ecafc83051d1410cacc867,Jackson Coelho
|
||||
unifiedStorageSearchSprinkles,2024-12-18T17:00:54Z,,4837585cab0fd84184a8c6f5d6891f442a2b95f1,owensmallwood
|
||||
prometheusSpecialCharsInLabelValues,2024-12-18T21:31:08Z,,721c50a304588ebd7cea76e301ec0f68a5a55d68,Nick Richmond
|
||||
unifiedStorageSearchUI,2024-12-19T18:21:48Z,,a8f347144ddc16f2033fdeb4f3474e49239ba7ab,Scott Lepper
|
||||
playlistsReconciler,2024-12-20T03:09:31Z,,24bf337c562dc9b9d8684cc9acb7ea171ea83414,Charandas
|
||||
|
||||
|
Generated
-1
@@ -148,7 +148,6 @@ alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true
|
||||
improvedExternalSessionHandling,GA,@grafana/identity-access-team,false,false,false
|
||||
useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,false
|
||||
rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false
|
||||
unifiedStorageSearchSprinkles,experimental,@grafana/search-and-storage,false,false,false
|
||||
managedDualWriter,experimental,@grafana/search-and-storage,false,false,false
|
||||
pluginsSriChecks,GA,@grafana/plugins-platform-backend,false,false,false
|
||||
unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false
|
||||
|
||||
|
Generated
-4
@@ -455,10 +455,6 @@ const (
|
||||
// Enables the new role picker drawer design
|
||||
FlagRolePickerDrawer = "rolePickerDrawer"
|
||||
|
||||
// FlagUnifiedStorageSearchSprinkles
|
||||
// Enable sprinkles on unified storage search
|
||||
FlagUnifiedStorageSearchSprinkles = "unifiedStorageSearchSprinkles"
|
||||
|
||||
// FlagManagedDualWriter
|
||||
// Pick the dual write mode from database configs
|
||||
FlagManagedDualWriter = "managedDualWriter"
|
||||
|
||||
-13
@@ -3723,19 +3723,6 @@
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "unifiedStorageSearchSprinkles",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-12-18T17:00:54Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable sprinkles on unified storage search",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/search-and-storage",
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "unifiedStorageSearchUI",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Colors struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
type ThemeDefinition struct {
|
||||
Colors Colors `json:"colors"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
themesPath := filepath.Join("..", "..", "..", "packages", "grafana-data", "src", "themes", "themeDefinitions")
|
||||
|
||||
// Check if the themes directory exists
|
||||
if _, err := os.Stat(themesPath); os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "Themes directory not found: %s\n", themesPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
output := `// Code generated by go generate; DO NOT EDIT.
|
||||
|
||||
package pref
|
||||
|
||||
var themes = []ThemeDTO{
|
||||
{ID: "light", Type: "light"},
|
||||
{ID: "dark", Type: "dark"},
|
||||
{ID: "system", Type: "dark"},
|
||||
`
|
||||
|
||||
err := filepath.WalkDir(themesPath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only process json files
|
||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".json") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileBytes, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading file %s: %v\n", path, readErr)
|
||||
return nil // Continue processing other files
|
||||
}
|
||||
|
||||
var themeDef ThemeDefinition
|
||||
jsonErr := json.Unmarshal(fileBytes, &themeDef)
|
||||
if jsonErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing JSON from %s: %v\n", path, jsonErr)
|
||||
return nil // Continue processing other files
|
||||
}
|
||||
|
||||
themeId := themeDef.Id
|
||||
themeType := "dark" // default fallback
|
||||
if themeDef.Colors.Mode != "" {
|
||||
themeType = themeDef.Colors.Mode
|
||||
}
|
||||
|
||||
output += fmt.Sprintf("\t{ID: %q, Type: %q, IsExtra: true},\n", themeId, themeType)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error walking themes directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
output += "}\n"
|
||||
|
||||
// Write the generated file
|
||||
outputPath := filepath.Join("themes_generated.go")
|
||||
if err := os.WriteFile(outputPath, []byte(output), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing output file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully generated themes_generated.go\n")
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:generate go run generate_themes.go
|
||||
|
||||
package pref
|
||||
|
||||
type ThemeDTO struct {
|
||||
@@ -6,24 +8,6 @@ type ThemeDTO struct {
|
||||
IsExtra bool `json:"isExtra"`
|
||||
}
|
||||
|
||||
var themes = []ThemeDTO{
|
||||
{ID: "light", Type: "light"},
|
||||
{ID: "dark", Type: "dark"},
|
||||
{ID: "system", Type: "dark"},
|
||||
{ID: "debug", Type: "dark", IsExtra: true},
|
||||
{ID: "aubergine", Type: "dark", IsExtra: true},
|
||||
{ID: "desertbloom", Type: "light", IsExtra: true},
|
||||
{ID: "gildedgrove", Type: "dark", IsExtra: true},
|
||||
{ID: "mars", Type: "dark", IsExtra: true},
|
||||
{ID: "matrix", Type: "dark", IsExtra: true},
|
||||
{ID: "sapphiredusk", Type: "dark", IsExtra: true},
|
||||
{ID: "synthwave", Type: "dark", IsExtra: true},
|
||||
{ID: "tron", Type: "dark", IsExtra: true},
|
||||
{ID: "victorian", Type: "dark", IsExtra: true},
|
||||
{ID: "zen", Type: "light", IsExtra: true},
|
||||
{ID: "gloom", Type: "dark", IsExtra: true},
|
||||
}
|
||||
|
||||
func GetThemeByID(id string) *ThemeDTO {
|
||||
for _, theme := range themes {
|
||||
if theme.ID == id {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
|
||||
package pref
|
||||
|
||||
var themes = []ThemeDTO{
|
||||
{ID: "light", Type: "light"},
|
||||
{ID: "dark", Type: "dark"},
|
||||
{ID: "system", Type: "dark"},
|
||||
{ID: "aubergine", Type: "dark", IsExtra: true},
|
||||
{ID: "debug", Type: "dark", IsExtra: true},
|
||||
{ID: "desertbloom", Type: "light", IsExtra: true},
|
||||
{ID: "gildedgrove", Type: "dark", IsExtra: true},
|
||||
{ID: "gloom", Type: "dark", IsExtra: true},
|
||||
{ID: "mars", Type: "dark", IsExtra: true},
|
||||
{ID: "matrix", Type: "dark", IsExtra: true},
|
||||
{ID: "sapphiredusk", Type: "dark", IsExtra: true},
|
||||
{ID: "synthwave", Type: "dark", IsExtra: true},
|
||||
{ID: "tron", Type: "dark", IsExtra: true},
|
||||
{ID: "victorian", Type: "dark", IsExtra: true},
|
||||
{ID: "zen", Type: "light", IsExtra: true},
|
||||
}
|
||||
@@ -237,7 +237,6 @@ kubernetesFolders = true
|
||||
unifiedStorage = true
|
||||
unifiedStorageHistoryPruner = true
|
||||
unifiedStorageSearchPermissionFiltering = false
|
||||
unifiedStorageSearchSprinkles = false
|
||||
|
||||
[unified_storage]
|
||||
enable_search = true
|
||||
@@ -315,9 +314,6 @@ To enable it, add the following to your `custom.ini` under the `[feature_toggles
|
||||
; Used by the Grafana instance
|
||||
unifiedStorageSearchUI = true
|
||||
|
||||
; (optional) Allows you to sort dashboards by usage insights fields when using enterprise
|
||||
; unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
; Used by unified storage server
|
||||
enable_search = true
|
||||
@@ -934,7 +930,6 @@ Unified Search requires several feature flags to be enabled depending on the des
|
||||
| Feature Flag | Purpose | Stage | Required For |
|
||||
|--------------|---------|-------|--------------|
|
||||
| `unifiedStorageSearchUI` | Frontend search interface | Experimental | Grafana UI search |
|
||||
| `unifiedStorageSearchSprinkles` | Usage insights integration | Experimental | Dashboard usage sorting (Enterprise) |
|
||||
| `unifiedStorageSearchDualReaderEnabled` | Shadow traffic to unified search | Experimental | Shadow traffic during migration |
|
||||
|
||||
#### Unified Search Specific Configuration
|
||||
@@ -955,9 +950,6 @@ unifiedStorageSearchUI = true
|
||||
; Enable shadow traffic during migration (optional)
|
||||
unifiedStorageSearchDualReaderEnabled = true
|
||||
|
||||
; Enable usage insights sorting (Enterprise only)
|
||||
unifiedStorageSearchSprinkles = true
|
||||
|
||||
[unified_storage]
|
||||
; Enable core search functionality (required)
|
||||
enable_search = true
|
||||
|
||||
@@ -271,7 +271,7 @@ func grpcConn(address string, metrics *clientMetrics, clientKeepaliveTime time.D
|
||||
retryCfg := retryConfig{
|
||||
Max: 3,
|
||||
Backoff: time.Second,
|
||||
BackoffJitter: 0.5,
|
||||
BackoffJitter: 0.1,
|
||||
}
|
||||
unary = append(unary, unaryRetryInterceptor(retryCfg))
|
||||
unary = append(unary, unaryRetryInstrument(metrics.requestRetries))
|
||||
@@ -288,13 +288,15 @@ func grpcConn(address string, metrics *clientMetrics, clientKeepaliveTime time.D
|
||||
opts = append(opts, grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
// Use round_robin to balances requests more evenly over the available Storage server.
|
||||
// Use round_robin to balance requests more evenly over the available Storage server.
|
||||
opts = append(opts, grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`))
|
||||
|
||||
// Disable looking up service config from TXT DNS records.
|
||||
// This reduces the number of requests made to the DNS servers.
|
||||
opts = append(opts, grpc.WithDisableServiceConfig())
|
||||
|
||||
opts = append(opts, connectionBackoffOptions())
|
||||
|
||||
if clientKeepaliveTime > 0 {
|
||||
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: clientKeepaliveTime,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
@@ -44,3 +45,17 @@ func unaryRetryInstrument(metric *prometheus.CounterVec) grpc.UnaryClientInterce
|
||||
return invoker(ctx, method, req, resp, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// connectionBackoffOptions configures connection backoff parameters for faster recovery from
|
||||
// transient connection failures (e.g., during pod restarts).
|
||||
func connectionBackoffOptions() grpc.DialOption {
|
||||
return grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 100 * time.Millisecond,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 10 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 5 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestIntegrationTeamBindings(t *testing.T) {
|
||||
doTeamBindingCRUDTestsUsingTheNewAPIs(t, helper, team, user)
|
||||
|
||||
if mode < 3 {
|
||||
doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper, mode)
|
||||
doTeamBindingCRUDTestsUsingTheLegacyAPIs(t, helper)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -84,13 +84,15 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
})
|
||||
|
||||
// Create the team binding
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
|
||||
defer func() {
|
||||
_ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
createdSpec := created.Object["spec"].(map[string]interface{})
|
||||
require.Equal(t, user.GetName(), createdSpec["subject"].(map[string]interface{})["name"])
|
||||
require.Equal(t, team.GetName(), createdSpec["teamRef"].(map[string]interface{})["name"])
|
||||
@@ -115,6 +117,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
// Update the team binding
|
||||
toUpdate := toCreate.DeepCopy()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member"
|
||||
toUpdate.Object["metadata"].(map[string]interface{})["name"] = createdUID
|
||||
updated, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, updated)
|
||||
@@ -164,9 +167,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
|
||||
@@ -185,9 +186,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = ""
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
toCreate := createTeamBindingObject(helper, "", team.GetName())
|
||||
|
||||
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
@@ -205,9 +204,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = ""
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), "")
|
||||
|
||||
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
@@ -225,9 +222,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
toCreate.Object["spec"].(map[string]interface{})["permission"] = "invalid"
|
||||
|
||||
_, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
@@ -245,17 +240,31 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
} {
|
||||
t.Run(fmt.Sprintf("with basic role_%s", u.Identity.GetOrgRole()), func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create the team binding using admin
|
||||
adminClient := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: helper.Org1.Admin,
|
||||
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
created, err := adminClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
_ = adminClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
teamBindingClient := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: u,
|
||||
Namespace: helper.Namespacer(helper.Org1.Admin.Identity.GetOrgID()),
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
toUpdate := created.DeepCopy()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["permission"] = "member"
|
||||
_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
_, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
|
||||
var statusErr *errors.StatusError
|
||||
@@ -273,10 +282,8 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toUpdate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
toUpdate.Object["metadata"].(map[string]interface{})["name"] = "invalid-team-binding-name"
|
||||
toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
var statusErr *errors.StatusError
|
||||
@@ -293,15 +300,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
// Create the team binding if it doesn't already exist
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
_ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
toUpdate := toCreate.DeepCopy()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = "test-team-2"
|
||||
_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName()
|
||||
_, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
var statusErr *errors.StatusError
|
||||
require.ErrorAs(t, err, &statusErr)
|
||||
@@ -317,16 +327,19 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
// Create the team binding if it doesn't already exist
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
_ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
toUpdate := toCreate.DeepCopy()
|
||||
toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = "test-user-2"
|
||||
|
||||
_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
_, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
var statusErr *errors.StatusError
|
||||
require.ErrorAs(t, err, &statusErr)
|
||||
@@ -342,15 +355,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
// Create the team binding if it doesn't already exist
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
_ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
toUpdate := toCreate.DeepCopy()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["external"] = true
|
||||
_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName()
|
||||
_, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
var statusErr *errors.StatusError
|
||||
require.ErrorAs(t, err, &statusErr)
|
||||
@@ -366,17 +382,18 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
GVR: gvrTeamBindings,
|
||||
})
|
||||
|
||||
// Create the team binding if it doesn't already exist
|
||||
toCreate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toCreate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toCreate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
_, _ = teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
toCreate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
created, err := teamBindingClient.Resource.Create(ctx, toCreate, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
toUpdate := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
toUpdate.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = user.GetName()
|
||||
toUpdate.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = team.GetName()
|
||||
defer func() {
|
||||
_ = teamBindingClient.Resource.Delete(ctx, created.GetName(), metav1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
toUpdate := createTeamBindingObject(helper, user.GetName(), team.GetName())
|
||||
toUpdate.Object["spec"].(map[string]interface{})["permission"] = "invalid"
|
||||
_, err := teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
toUpdate.Object["metadata"].(map[string]interface{})["name"] = created.GetName()
|
||||
_, err = teamBindingClient.Resource.Update(ctx, toUpdate, metav1.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
var statusErr *errors.StatusError
|
||||
require.ErrorAs(t, err, &statusErr)
|
||||
@@ -385,7 +402,7 @@ func doTeamBindingCRUDTestsUsingTheNewAPIs(t *testing.T, helper *apis.K8sTestHel
|
||||
})
|
||||
}
|
||||
|
||||
func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper, mode rest.DualWriterMode) {
|
||||
func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTestHelper) {
|
||||
t.Run("should create team binding using legacy APIs and get it using the new APIs", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -499,3 +516,10 @@ func doTeamBindingCRUDTestsUsingTheLegacyAPIs(t *testing.T, helper *apis.K8sTest
|
||||
require.Equal(t, teamBindingName, teamBinding.GetName())
|
||||
})
|
||||
}
|
||||
|
||||
func createTeamBindingObject(helper *apis.K8sTestHelper, userName, teamName string) *unstructured.Unstructured {
|
||||
obj := helper.LoadYAMLOrJSONFile("testdata/teambinding-test-create-v0.yaml")
|
||||
obj.Object["spec"].(map[string]interface{})["subject"].(map[string]interface{})["name"] = userName
|
||||
obj.Object["spec"].(map[string]interface{})["teamRef"].(map[string]interface{})["name"] = teamName
|
||||
return obj
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apiVersion: iam.grafana.app/v0alpha1
|
||||
kind: TeamBinding
|
||||
metadata:
|
||||
name: test-team-binding-1
|
||||
generateName: test-team-binding-
|
||||
spec:
|
||||
subject:
|
||||
name: ""
|
||||
|
||||
Reference in New Issue
Block a user