diff --git a/pkg/services/store/resource/client_wrapper.go b/pkg/services/store/resource/client_wrapper.go new file mode 100644 index 00000000000..7eef6c144f7 --- /dev/null +++ b/pkg/services/store/resource/client_wrapper.go @@ -0,0 +1,30 @@ +package resource + +import ( + "github.com/fullstorydev/grpchan" + "github.com/fullstorydev/grpchan/inprocgrpc" + grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" + "google.golang.org/grpc" + + grpcUtils "github.com/grafana/grafana/pkg/services/store/entity/grpc" +) + +func NewResourceStoreClientLocal(server ResourceStoreServer) ResourceStoreClient { + channel := &inprocgrpc.Channel{} + + auth := &grpcUtils.Authenticator{} + + channel.RegisterService( + grpchan.InterceptServer( + &ResourceStore_ServiceDesc, + grpcAuth.UnaryServerInterceptor(auth.Authenticate), + grpcAuth.StreamServerInterceptor(auth.Authenticate), + ), + server, + ) + return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor)) +} + +func NewEntityStoreClientGRPC(channel *grpc.ClientConn) ResourceStoreClient { + return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor)) +} diff --git a/pkg/services/store/resource/tests/common_test.go b/pkg/services/store/resource/tests/common_test.go new file mode 100644 index 00000000000..c9ceb92aa7e --- /dev/null +++ b/pkg/services/store/resource/tests/common_test.go @@ -0,0 +1,105 @@ +package resource_server_tests + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/tracing" + + "github.com/grafana/grafana/pkg/components/satokengen" + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/org" + saAPI "github.com/grafana/grafana/pkg/services/serviceaccounts/api" + saTests "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" + "github.com/grafana/grafana/pkg/services/store/entity/db/dbimpl" + "github.com/grafana/grafana/pkg/services/store/resource" + "github.com/grafana/grafana/pkg/services/store/resource/sqlstash" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +func createServiceAccountAdminToken(t *testing.T, env *server.TestEnv) (string, *user.SignedInUser) { + t.Helper() + + account := saTests.SetupUserServiceAccount(t, env.SQLStore, env.Cfg, saTests.TestUser{ + Name: "grpc-server-sa", + Role: string(org.RoleAdmin), + Login: "grpc-server-sa", + IsServiceAccount: true, + }) + + keyGen, err := satokengen.New(saAPI.ServiceID) + require.NoError(t, err) + + _ = saTests.SetupApiKey(t, env.SQLStore, env.Cfg, saTests.TestApiKey{ + Name: "grpc-server-test", + Role: org.RoleAdmin, + OrgId: account.OrgID, + Key: keyGen.HashedKey, + ServiceAccountID: &account.ID, + }) + + return keyGen.ClientSecret, &user.SignedInUser{ + UserID: account.ID, + Email: account.Email, + Name: account.Name, + Login: account.Login, + OrgID: account.OrgID, + IsServiceAccount: account.IsServiceAccount, + } +} + +type testContext struct { + authToken string + client resource.ResourceStoreClient + user *user.SignedInUser + ctx context.Context +} + +func createTestContext(t *testing.T) testContext { + t.Helper() + + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + EnableFeatureToggles: []string{ + featuremgmt.FlagGrpcServer, + featuremgmt.FlagUnifiedStorage, + }, + AppModeProduction: false, // required for migrations to run + GRPCServerAddress: "127.0.0.1:0", // :0 for choosing the port automatically + }) + + _, env := testinfra.StartGrafanaEnv(t, dir, path) + + authToken, serviceAccountUser := createServiceAccountAdminToken(t, env) + + eDB, err := dbimpl.ProvideEntityDB(env.SQLStore, env.Cfg, env.FeatureToggles, nil) + require.NoError(t, err) + + err = eDB.Init() + require.NoError(t, err) + + traceConfig, err := tracing.ParseTracingConfig(env.Cfg) + require.NoError(t, err) + tracer, err := tracing.ProvideService(traceConfig) + require.NoError(t, err) + store, err := sqlstash.ProvideSQLResourceServer(eDB, tracer) + require.NoError(t, err) + + client := resource.NewResourceStoreClientLocal(store) + + return testContext{ + authToken: authToken, + client: client, + user: serviceAccountUser, + ctx: appcontext.WithUser(context.Background(), serviceAccountUser), + } +} diff --git a/pkg/services/store/resource/tests/server_integration_test.go b/pkg/services/store/resource/tests/server_integration_test.go new file mode 100644 index 00000000000..83305c25fc6 --- /dev/null +++ b/pkg/services/store/resource/tests/server_integration_test.go @@ -0,0 +1,84 @@ +package resource_server_tests + +import ( + _ "embed" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1" + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/services/store/resource" +) + +var ( + //go:embed testdata/dashboard-with-tags-b-g.json + dashboardWithTagsBlueGreen string + //go:embed testdata/dashboard-with-tags-r-g.json + dashboardWithTagsRedGreen string +) + +func TestIntegrationEntityServer(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + testCtx := createTestContext(t) + ctx := appcontext.WithUser(testCtx.ctx, testCtx.user) + + t.Run("should not retrieve non-existent objects", func(t *testing.T) { + resp, err := testCtx.client.GetResource(ctx, &resource.GetResourceRequest{ + Key: &resource.Key{ + Group: "X", + Namespace: "X", + }, + }) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Status) + require.Nil(t, resp.Value) + require.Equal(t, int32(http.StatusBadRequest), resp.Status.Code) + }) + + t.Run("insert an object", func(t *testing.T) { + var err error + key := &resource.Key{ + Namespace: "default", + Group: "playlists.grafana.app", + Resource: "Playlist", + Name: "x123", + } + sample := v0alpha1.Playlist{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + UID: types.UID("xyz"), + }, + TypeMeta: metav1.TypeMeta{ + Kind: key.Resource, + APIVersion: key.Group + "/v0alpha1", + }, + Spec: v0alpha1.Spec{ + Title: "hello", + }, + } + req := &resource.CreateRequest{ + Key: key, + } + req.Value, err = json.Marshal(sample) + require.NoError(t, err) + + fmt.Printf("%s", string(req.Value)) + + resp, err := testCtx.client.Create(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Nil(t, resp.Status) + require.True(t, resp.ResourceVersion > 0) // that it has a positive resource version + }) +} diff --git a/pkg/services/store/resource/tests/testdata/dashboard-with-tags-b-g.json b/pkg/services/store/resource/tests/testdata/dashboard-with-tags-b-g.json new file mode 100644 index 00000000000..5bbe8e786fb --- /dev/null +++ b/pkg/services/store/resource/tests/testdata/dashboard-with-tags-b-g.json @@ -0,0 +1,39 @@ +{ + "tags": [ + "blue", + "green" + ], + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 221, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 8, + "title": "Row title", + "type": "row" + } + ], + "schemaVersion": 36, + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "special ds", + "uid": "mocpwtR4k", + "version": 1, + "weekStart": "" +} diff --git a/pkg/services/store/resource/tests/testdata/dashboard-with-tags-r-g.json b/pkg/services/store/resource/tests/testdata/dashboard-with-tags-r-g.json new file mode 100644 index 00000000000..67fdda77352 --- /dev/null +++ b/pkg/services/store/resource/tests/testdata/dashboard-with-tags-r-g.json @@ -0,0 +1,39 @@ +{ + "tags": [ + "red", + "green" + ], + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 221, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 8, + "title": "Row title", + "type": "row" + } + ], + "schemaVersion": 36, + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "special ds", + "uid": "mocpwtR4k", + "version": 1, + "weekStart": "" +} diff --git a/pkg/services/store/resource/utils.go b/pkg/services/store/resource/utils.go new file mode 100644 index 00000000000..ad4a6f360ca --- /dev/null +++ b/pkg/services/store/resource/utils.go @@ -0,0 +1,14 @@ +package resource + +import ( + "fmt" + "net/http" +) + +func badRequest(format string, a ...any) *StatusResult { + return &StatusResult{ + Status: "Failure", + Message: fmt.Sprintf(format, a...), + Code: http.StatusBadRequest, + } +}