diff --git a/pkg/storage/unified/e2e/grafana.go b/pkg/storage/unified/e2e/grafana.go new file mode 100644 index 00000000000..3d1971d5a19 --- /dev/null +++ b/pkg/storage/unified/e2e/grafana.go @@ -0,0 +1,87 @@ +package e2e + +import ( + "fmt" + "net/url" + "os" + + "github.com/grafana/e2e" + gapi "github.com/grafana/grafana-api-golang-client" +) + +const ( + grafanaINI = ` +app_mode = development +target = all + +[grafana-apiserver] +address = 0.0.0.0:10000 +storage_type = unified-grpc + +[unified_storage.dashboards.dashboard.grafana.app] +dualWriterMode = 5 + +[unified_storage.folders.folder.grafana.app] +dualWriterMode = 5 +` + defaultGrafanaImage = "grafana/grafana:main" + grafanaBinary = "/run.sh" + grafanaHTTPPort = 3000 + grafanaGRPCPort = 10000 +) + +type GrafanaService struct { + *e2e.HTTPService +} + +func NewGrafanaService(name string, flags, envVars map[string]string) *GrafanaService { + svc := &GrafanaService{ + HTTPService: e2e.NewHTTPService( + name, + GetGrafanaImage(), + e2e.NewCommandWithoutEntrypoint(grafanaBinary, e2e.BuildArgs(flags)...), + e2e.NewHTTPReadinessProbe(grafanaHTTPPort, "/ready", 200, 299), + grafanaHTTPPort, + grafanaGRPCPort), + } + + svc.ConcreteService.SetEnvVars(envVars) + + return svc +} + +func (g *GrafanaService) GRPCEndpoint() string { + return g.HTTPService.NetworkEndpoint(grafanaGRPCPort) +} + +type GrafanaClient struct { + *gapi.Client +} + +func NewGrafanaClient(host string, orgID int64) (*GrafanaClient, error) { + cfg := gapi.Config{ + BasicAuth: url.UserPassword("admin", "admin"), + OrgID: orgID, + HTTPHeaders: map[string]string{ + "X-Disable-Provenance": "true", + }, + } + + client, err := gapi.New(fmt.Sprintf("http://%s/", host), cfg) + if err != nil { + return nil, err + } + + return &GrafanaClient{ + Client: client, + }, nil +} + +// GetDefaultImage returns the Docker image to use to run the Grafana.. +func GetGrafanaImage() string { + if img := os.Getenv("GRAFANA_IMAGE"); img != "" { + return img + } + + return defaultGrafanaImage +} diff --git a/pkg/storage/unified/e2e/postgres.go b/pkg/storage/unified/e2e/postgres.go new file mode 100644 index 00000000000..68eea764b58 --- /dev/null +++ b/pkg/storage/unified/e2e/postgres.go @@ -0,0 +1,41 @@ +package e2e + +import ( + "os" + + "github.com/grafana/e2e" +) + +const ( + defaultPostgresImage = "postgres:16.4" + postgresHTTPPort = 5432 +) + +// GetDefaultImage returns the Docker image to use to run the Postgres.. +func GetPostgresImage() string { + if img := os.Getenv("POSTGRES_IMAGE"); img != "" { + return img + } + + return defaultPostgresImage +} + +type PostgresService struct { + *e2e.HTTPService +} + +func NewPostgresService(name string, envVars map[string]string) *PostgresService { + svc := &PostgresService{ + HTTPService: e2e.NewHTTPService( + name, + GetPostgresImage(), + nil, + nil, + postgresHTTPPort, + ), + } + + svc.SetEnvVars(envVars) + + return svc +} diff --git a/pkg/storage/unified/e2e/storage.go b/pkg/storage/unified/e2e/storage.go new file mode 100644 index 00000000000..96e7c9a206b --- /dev/null +++ b/pkg/storage/unified/e2e/storage.go @@ -0,0 +1,57 @@ +package e2e + +import ( + "github.com/grafana/e2e" +) + +const ( + storageINI = ` +app_mode = development +target = storage-server + +[grafana-apiserver] +storage_type = unified + +[database] +type = postgres +host = postgres:5432 +name = grafana +user = admin +password = admin +ssl_mode = disable +` +) + +type StorageService struct { + *e2e.ConcreteService +} + +func NewStorageService(name string, flags, envVars map[string]string) *StorageService { + args := append([]string{"target"}, e2e.BuildArgs(flags)...) + svc := &StorageService{ + ConcreteService: e2e.NewConcreteService( + name, + GetGrafanaImage(), + e2e.NewCommandWithoutEntrypoint(grafanaBinary, args...), + e2e.NewTCPReadinessProbe(10000), + 10000, + ), + } + + svc.SetEnvVars(envVars) + svc.Endpoint(10000) + + return svc +} + +// TODO: remove this method when the storage-server module has a graceful shutdown on docker stop +// the following method is implemented because the storage-server module is not shutting down gracefully +// https://github.com/grafana/search-and-storage-team/issues/228 +func (s *StorageService) Stop() error { + _ = s.ConcreteService.Stop() + return nil +} + +func (s *StorageService) GRPCEndpoint() string { + return s.NetworkEndpoint(grafanaGRPCPort) +} diff --git a/pkg/storage/unified/e2e/unified_scenario.go b/pkg/storage/unified/e2e/unified_scenario.go new file mode 100644 index 00000000000..8125d8da65c --- /dev/null +++ b/pkg/storage/unified/e2e/unified_scenario.go @@ -0,0 +1,141 @@ +package e2e + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/grafana/e2e" +) + +type UnifiedScenario struct { + *e2e.Scenario + + Grafana *GrafanaService + UnifiedStorage *StorageService + Postgres *PostgresService +} + +func NewUnifiedScenario() (*UnifiedScenario, error) { + s, err := e2e.NewScenario(GetNetworkName()) + if err != nil { + return nil, err + } + + return &UnifiedScenario{ + Scenario: s, + }, nil +} + +func (s *UnifiedScenario) NewGrafanaService(name string, grpcEndpoint string) (*GrafanaService, error) { + flags := map[string]string{ + "--config": fmt.Sprintf("/shared/%s.ini", name), + } + fts := []string{ + "grafanaAPIServerWithExperimentalAPIs", + "kubernetesClientDashboardsFolders", + "grafanaAPIServerEnsureKubectlAccess", + "unifiedStorageSearch", + "unifiedStorageSearchUI", + "kubernetesCliDashboards", + "kubernetesDashboardsAPI", + "kubernetesFolders", + "unified_storage", + } + envVars := map[string]string{ + "GF_PATHS_CONFIG": fmt.Sprintf("/shared/%s.ini", name), + "GF_FEATURE_TOGGLES_ENABLE": strings.Join(fts, ","), + "GF_GRAFANA_APISERVER_ADDRESS": grpcEndpoint, + "GF_GRAFANA_APISERVER_STORAGE_TYPE": "unified-grpc", + "GF_DATABASE_TYPE": "postgres", + "GF_DATABASE_HOST": "postgres:5432", + "GF_DATABASE_NAME": "grafana", + "GF_DATABASE_USER": "admin", + "GF_DATABASE_PASSWORD": "admin", + "GF_DATABASE_SSL_MODE": "disable", + } + + if err := s.loadCfg(name, grafanaINI); err != nil { + return nil, err + } + g := NewGrafanaService(name, flags, envVars) + s.Grafana = g + return g, nil +} + +func (s *UnifiedScenario) NewGrafanaClient(grafanaName string, orgID int64) (*GrafanaClient, error) { + g := s.Grafana + return NewGrafanaClient(g.HTTPEndpoint(), orgID) +} + +func (s *UnifiedScenario) NewStorageService(name string) (*StorageService, error) { + flags := map[string]string{ + "--config": "/shared/storage.ini", + } + envVars := map[string]string{ + "GF_PATHS_CONFIG": "/shared/storage.ini", + "GF_DEFAULT_TARGET": "storage-server", + "GF_GRPC_SERVER_ADDRESS": "0.0.0.0:10000", + "GF_FEATURE_TOGGLES_ENABLE": "unifiedStorage,unifiedStorageSearch", + "GF_RESOURCE_API_DB_TYPE": "postgres", + "GF_RESOURCE_API_DB_HOST": "postgres:5432", + "GF_RESOURCE_API_DB_NAME": "grafana", + "GF_RESOURCE_API_DB_USER": "admin", + "GF_RESOURCE_API_DB_PASS": "admin", + "GF_DATABASE_TYPE": "postgres", + "GF_DATABASE_HOST": "postgres:5432", + "GF_DATABASE_NAME": "grafana", + "GF_DATABASE_USER": "admin", + "GF_DATABASE_PASSWORD": "admin", + "GF_DATABASE_SSL_MODE": "disable", + } + + if err := s.loadCfg(name, storageINI); err != nil { + return nil, err + } + us := NewStorageService(name, flags, envVars) + s.UnifiedStorage = us + return us, nil +} + +func (s *UnifiedScenario) NewPostgresService(name string) *PostgresService { + ps := NewPostgresService(name, map[string]string{ + "POSTGRES_USER": "admin", + "POSTGRES_PASSWORD": "admin", + "POSTGRES_DB": "grafana", + }) + s.Postgres = ps + + return ps +} + +func (s *UnifiedScenario) loadCfg(svc, content string) error { + dst := fmt.Sprintf("%s/%s.ini", s.SharedDir(), svc) + destination, err := os.Create(filepath.Clean(dst)) + if err != nil { + return err + } + + _, err = destination.Write([]byte(content)) + if err != nil { + return err + } + + return nil +} + +const ( + defaultNetworkName = "e2e-grafana-unified" +) + +// GetNetworkName returns the docker network name to run tests within. +func GetNetworkName() string { + // If the E2E_NETWORK_NAME is set, use that for the network name. + // Otherwise, return the default network name. + if os.Getenv("E2E_NETWORK_NAME") != "" { + return os.Getenv("E2E_NETWORK_NAME") + } + + return defaultNetworkName +} diff --git a/pkg/storage/unified/e2e/unified_test.go b/pkg/storage/unified/e2e/unified_test.go new file mode 100644 index 00000000000..8393471ddb0 --- /dev/null +++ b/pkg/storage/unified/e2e/unified_test.go @@ -0,0 +1,61 @@ +package e2e + +import ( + "testing" + + gapi "github.com/grafana/grafana-api-golang-client" + "github.com/stretchr/testify/require" +) + +func TestUnifiedStorage(t *testing.T) { + // TODO: remove this line when ci pipeline is ready + t.Skip("skipping e2e test") + + t.Run("Create folder and dashboard", func(t *testing.T) { + s, err := NewUnifiedScenario() + require.NoError(t, err) + + postgres := s.NewPostgresService("postgres") + require.NoError(t, s.Scenario.StartAndWaitReady(postgres)) + + storage, err := s.NewStorageService("storage") + require.NoError(t, err) + require.NoError(t, s.Scenario.StartAndWaitReady(storage)) + + grafana, err := s.NewGrafanaService("grafana", storage.GRPCEndpoint()) + require.NoError(t, err) + require.NoError(t, s.Scenario.StartAndWaitReady(grafana)) + + const org1 = 1 + gorg1, err := s.NewGrafanaClient("grafana", org1) + require.NoError(t, err) + + folderOrg1, err := gorg1.NewFolder("test-folder-1") + require.NoError(t, err) + + const uid = "test-dashboard-1" + dashboard := gapi.Dashboard{ + Model: map[string]interface{}{ + "uid": uid, + "title": "Test Dashboard 1", + }, + FolderID: folderOrg1.ID, + FolderUID: folderOrg1.UID, + Overwrite: false, + Message: "Test Dashboard 1", + } + + dashboardOrg1, err := gorg1.NewDashboard(dashboard) + require.NoError(t, err) + + newDashboard, err := gorg1.DashboardByUID(uid) + require.NoError(t, err) + require.Equal(t, dashboardOrg1.UID, newDashboard.Model["uid"]) + + newFolder, err := gorg1.Folder(folderOrg1.ID) + require.NoError(t, err) + require.Equal(t, folderOrg1.UID, newFolder.UID) + + require.NoError(t, s.Scenario.Stop(grafana, storage, postgres)) + }) +}