Merge remote-tracking branch 'origin' into axelav/dash-validator-app-mvp

This commit is contained in:
alexandra vargas
2026-01-13 14:58:15 +01:00
120 changed files with 6448 additions and 2650 deletions
+1
View File
@@ -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"
+1 -1
View File
@@ -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)
})
}
}
+10 -2
View File
@@ -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
}
+53
View File
@@ -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,
})
}
+49 -8
View File
@@ -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
}
-7
View File
@@ -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
1 #name created deleted hash author
409 kubernetesCliDashboards 2024-12-13T22:55:43Z 2025-02-18T23:11:26Z 8f6e9f8ed0a5024a510cc337c9f1e6972bfb23d4 Stephanie Hingtgen
410 useV2DashboardsAPI 2024-12-17T21:17:09Z 2025-03-12T17:43:32Z 070f0e4457c5967102ef157197073dc2662f6fb8 Dominik Prokop
411 investigationsBackend 2024-12-18T08:31:03Z f46c07aba7b6faccd2ecafc83051d1410cacc867 Jackson Coelho
unifiedStorageSearchSprinkles 2024-12-18T17:00:54Z 4837585cab0fd84184a8c6f5d6891f442a2b95f1 owensmallwood
412 prometheusSpecialCharsInLabelValues 2024-12-18T21:31:08Z 721c50a304588ebd7cea76e301ec0f68a5a55d68 Nick Richmond
413 unifiedStorageSearchUI 2024-12-19T18:21:48Z a8f347144ddc16f2033fdeb4f3474e49239ba7ab Scott Lepper
414 playlistsReconciler 2024-12-20T03:09:31Z 24bf337c562dc9b9d8684cc9acb7ea171ea83414 Charandas
-1
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
148 improvedExternalSessionHandling GA @grafana/identity-access-team false false false
149 useSessionStorageForRedirection GA @grafana/identity-access-team false false false
150 rolePickerDrawer experimental @grafana/identity-access-team false false false
unifiedStorageSearchSprinkles experimental @grafana/search-and-storage false false false
151 managedDualWriter experimental @grafana/search-and-storage false false false
152 pluginsSriChecks GA @grafana/plugins-platform-backend false false false
153 unifiedStorageBigObjectsSupport experimental @grafana/search-and-storage false false false
-4
View File
@@ -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
View File
@@ -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")
}
+2 -18
View File
@@ -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},
}
-8
View File
@@ -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
+4 -2
View File
@@ -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,
+15
View File
@@ -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: ""