mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudMigrations: Add support for migration of plugin resources (#95612)
* start plugins migration * more plugin work * add warning * fakepluginsettings test * tests get plugins * lint * load logos * go lint * get all plugins once * locales * josh suggestion to inject query in rtk * more plugin filters * remove datasource warning * access control for plugins * remove unused method * lint * use gcom list
This commit is contained in:
parent
6ca6ad4df7
commit
1699dfa307
2
go.mod
2
go.mod
@ -86,7 +86,7 @@ require (
|
|||||||
github.com/grafana/grafana-app-sdk v0.23.1 // @grafana/grafana-app-platform-squad
|
github.com/grafana/grafana-app-sdk v0.23.1 // @grafana/grafana-app-platform-squad
|
||||||
github.com/grafana/grafana-aws-sdk v0.31.5 // @grafana/aws-datasources
|
github.com/grafana/grafana-aws-sdk v0.31.5 // @grafana/aws-datasources
|
||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 // @grafana/partner-datasources
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 // @grafana/partner-datasources
|
||||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 // @grafana/grafana-operator-experience-squad
|
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 // @grafana/grafana-operator-experience-squad
|
||||||
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
|
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
|
||||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
|
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.260.3 // @grafana/plugins-platform-backend
|
github.com/grafana/grafana-plugin-sdk-go v0.260.3 // @grafana/plugins-platform-backend
|
||||||
|
4
go.sum
4
go.sum
@ -2323,8 +2323,8 @@ github.com/grafana/grafana-aws-sdk v0.31.5 h1:4HpMQx7n4Qqoi7Bgu8KHQ2QKT9fYYdHilX
|
|||||||
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
|
github.com/grafana/grafana-aws-sdk v0.31.5/go.mod h1:5p4Cjyr5ZiR6/RT2nFWkJ8XpIKgX4lAUmUMu70m2yCM=
|
||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGAnI27HhQkaLttzbPE=
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGAnI27HhQkaLttzbPE=
|
||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0=
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0=
|
||||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 h1:F0O9eTy4jHjEd1Z3/qIza2GdY7PYpTddUeaq9p3NKGU=
|
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0 h1:S4kHwr//AqhtL9xHBtz1gqVgZQeCRGTxjgsRBAkpjKY=
|
||||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0/go.mod h1:bd6Cm06EK0MzRO5ahUpbDz1SxNOKu+fzladbaRPHZPY=
|
github.com/grafana/grafana-cloud-migration-snapshot v1.6.0/go.mod h1:rWNhyxYkgiXgV7xZ4yOQzMV08yikO8L8S8M5KNoQNpA=
|
||||||
github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA=
|
github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA=
|
||||||
github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
||||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
||||||
|
@ -131,6 +131,7 @@ const (
|
|||||||
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
|
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
|
||||||
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
|
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
|
||||||
MuteTimingType MigrateDataType = "MUTE_TIMING"
|
MuteTimingType MigrateDataType = "MUTE_TIMING"
|
||||||
|
PluginDataType MigrateDataType = "PLUGIN"
|
||||||
)
|
)
|
||||||
|
|
||||||
// swagger:enum ItemStatus
|
// swagger:enum ItemStatus
|
||||||
@ -158,7 +159,6 @@ const (
|
|||||||
ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT"
|
ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT"
|
||||||
ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE"
|
ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE"
|
||||||
ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR"
|
ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR"
|
||||||
ErrOnlyCoreDataSources ItemErrorCode = "ONLY_CORE_DATA_SOURCES"
|
|
||||||
ErrGeneric ItemErrorCode = "GENERIC_ERROR"
|
ErrGeneric ItemErrorCode = "GENERIC_ERROR"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/kvstore"
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/authapi"
|
"github.com/grafana/grafana/pkg/services/authapi"
|
||||||
"github.com/grafana/grafana/pkg/services/authapi/fake"
|
"github.com/grafana/grafana/pkg/services/authapi/fake"
|
||||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||||
@ -32,6 +33,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/gcom"
|
"github.com/grafana/grafana/pkg/services/gcom"
|
||||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert"
|
"github.com/grafana/grafana/pkg/services/ngalert"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets"
|
"github.com/grafana/grafana/pkg/services/secrets"
|
||||||
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||||
@ -68,6 +70,8 @@ type Service struct {
|
|||||||
dashboardService dashboards.DashboardService
|
dashboardService dashboards.DashboardService
|
||||||
folderService folder.Service
|
folderService folder.Service
|
||||||
pluginStore pluginstore.Store
|
pluginStore pluginstore.Store
|
||||||
|
accessControl accesscontrol.AccessControl
|
||||||
|
pluginSettingsService pluginsettings.Service
|
||||||
secretsService secrets.Service
|
secretsService secrets.Service
|
||||||
kvStore *kvstore.NamespacedKVStore
|
kvStore *kvstore.NamespacedKVStore
|
||||||
libraryElementsService libraryelements.Service
|
libraryElementsService libraryelements.Service
|
||||||
@ -105,6 +109,8 @@ func ProvideService(
|
|||||||
dashboardService dashboards.DashboardService,
|
dashboardService dashboards.DashboardService,
|
||||||
folderService folder.Service,
|
folderService folder.Service,
|
||||||
pluginStore pluginstore.Store,
|
pluginStore pluginstore.Store,
|
||||||
|
pluginSettingsService pluginsettings.Service,
|
||||||
|
accessControl accesscontrol.AccessControl,
|
||||||
kvStore kvstore.KVStore,
|
kvStore kvstore.KVStore,
|
||||||
libraryElementsService libraryelements.Service,
|
libraryElementsService libraryelements.Service,
|
||||||
ngAlert *ngalert.AlertNG,
|
ngAlert *ngalert.AlertNG,
|
||||||
@ -125,6 +131,8 @@ func ProvideService(
|
|||||||
dashboardService: dashboardService,
|
dashboardService: dashboardService,
|
||||||
folderService: folderService,
|
folderService: folderService,
|
||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
|
pluginSettingsService: pluginSettingsService,
|
||||||
|
accessControl: accessControl,
|
||||||
kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"),
|
kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"),
|
||||||
libraryElementsService: libraryElementsService,
|
libraryElementsService: libraryElementsService,
|
||||||
ngAlert: ngAlert,
|
ngAlert: ngAlert,
|
||||||
@ -588,13 +596,7 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps
|
|||||||
s.log.Error("unexpected GMS snapshot state: %s", snapshotMeta.State)
|
s.log.Error("unexpected GMS snapshot state: %s", snapshotMeta.State)
|
||||||
return snapshot, nil
|
return snapshot, nil
|
||||||
}
|
}
|
||||||
|
resources := snapshotMeta.Results
|
||||||
// For 11.2 we only support core data sources. Apply a warning for any non-core ones before storing.
|
|
||||||
resources, err := s.getResourcesWithPluginWarnings(ctx, snapshotMeta.Results)
|
|
||||||
if err != nil {
|
|
||||||
// treat this as non-fatal since the migration still succeeded
|
|
||||||
s.log.Error("error applying plugin warnings, please open a bug report: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the errors for resources with errors at migration
|
// Log the errors for resources with errors at migration
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
@ -901,40 +903,3 @@ func (s *Service) deleteLocalFiles(snapshots []cloudmigration.CloudMigrationSnap
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getResourcesWithPluginWarnings iterates through each resource and, if a non-core datasource, applies a warning that we only support core
|
|
||||||
func (s *Service) getResourcesWithPluginWarnings(ctx context.Context, results []cloudmigration.CloudMigrationResource) ([]cloudmigration.CloudMigrationResource, error) {
|
|
||||||
dsList, err := s.dsService.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting all data sources: %w", err)
|
|
||||||
}
|
|
||||||
dsMap := make(map[string]*datasources.DataSource, len(dsList))
|
|
||||||
for i := 0; i < len(dsList); i++ {
|
|
||||||
dsMap[dsList[i].UID] = dsList[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(results); i++ {
|
|
||||||
r := results[i]
|
|
||||||
|
|
||||||
if r.Type == cloudmigration.DatasourceDataType &&
|
|
||||||
r.Error == "" { // any error returned by GMS takes priority
|
|
||||||
ds, ok := dsMap[r.RefID]
|
|
||||||
if !ok {
|
|
||||||
s.log.Error("data source with id %s was not found in data sources list", r.RefID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
p, found := s.pluginStore.Plugin(ctx, ds.Type)
|
|
||||||
// if the plugin is not found, it means it was uninstalled, meaning it wasn't core
|
|
||||||
if !p.IsCorePlugin() || !found {
|
|
||||||
r.Status = cloudmigration.ItemStatusWarning
|
|
||||||
r.ErrorCode = cloudmigration.ErrOnlyCoreDataSources
|
|
||||||
r.Error = "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack."
|
|
||||||
}
|
|
||||||
|
|
||||||
results[i] = r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
@ -24,8 +24,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||||
"github.com/grafana/grafana/pkg/services/cloudmigration/gmsclient"
|
"github.com/grafana/grafana/pkg/services/cloudmigration/gmsclient"
|
||||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
|
||||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||||
@ -39,6 +37,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||||
@ -447,146 +446,6 @@ func Test_SortFolders(t *testing.T) {
|
|||||||
require.Equal(t, expected, sortedFolders)
|
require.Equal(t, expected, sortedFolders)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_NonCoreDataSourcesHaveWarning(t *testing.T) {
|
|
||||||
s := setUpServiceTest(t, false).(*Service)
|
|
||||||
|
|
||||||
// Insert a processing snapshot into the database before we start so we query GMS
|
|
||||||
createTokenResp, err := s.CreateToken(context.Background())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, createTokenResp.Token)
|
|
||||||
|
|
||||||
sess, err := s.store.CreateMigrationSession(context.Background(), cloudmigration.CloudMigrationSession{
|
|
||||||
AuthToken: createTokenResp.Token,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
snapshotUid, err := s.store.CreateSnapshot(context.Background(), cloudmigration.CloudMigrationSnapshot{
|
|
||||||
UID: uuid.NewString(),
|
|
||||||
SessionUID: sess.UID,
|
|
||||||
Status: cloudmigration.SnapshotStatusProcessing,
|
|
||||||
GMSSnapshotUID: "gms uid",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// GMS should return: a core ds, a non-core ds, a non-core ds with an error, and a ds that has been uninstalled
|
|
||||||
gmsClientMock := &gmsClientMock{
|
|
||||||
getSnapshotResponse: &cloudmigration.GetSnapshotStatusResponse{
|
|
||||||
State: cloudmigration.SnapshotStateFinished,
|
|
||||||
Results: []cloudmigration.CloudMigrationResource{
|
|
||||||
{
|
|
||||||
Name: "1 name",
|
|
||||||
ParentName: "1 parent name",
|
|
||||||
Type: cloudmigration.DatasourceDataType,
|
|
||||||
RefID: "1", // this will be core
|
|
||||||
Status: cloudmigration.ItemStatusOK,
|
|
||||||
SnapshotUID: snapshotUid,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "2 name",
|
|
||||||
ParentName: "",
|
|
||||||
Type: cloudmigration.DatasourceDataType,
|
|
||||||
RefID: "2", // this will be non-core
|
|
||||||
Status: cloudmigration.ItemStatusOK,
|
|
||||||
SnapshotUID: snapshotUid,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "3 name",
|
|
||||||
ParentName: "3 parent name",
|
|
||||||
Type: cloudmigration.DatasourceDataType,
|
|
||||||
RefID: "3", // this will be non-core with an error
|
|
||||||
Status: cloudmigration.ItemStatusError,
|
|
||||||
Error: "please don't overwrite me",
|
|
||||||
SnapshotUID: snapshotUid,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "4 name",
|
|
||||||
ParentName: "4 folder name",
|
|
||||||
Type: cloudmigration.DatasourceDataType,
|
|
||||||
RefID: "4", // this will be deleted
|
|
||||||
Status: cloudmigration.ItemStatusOK,
|
|
||||||
SnapshotUID: snapshotUid,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
s.gmsClient = gmsClientMock
|
|
||||||
|
|
||||||
// Update the internal plugin store and ds store with seed data matching the descriptions above
|
|
||||||
s.pluginStore = pluginstore.NewFakePluginStore([]pluginstore.Plugin{
|
|
||||||
{
|
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: "1",
|
|
||||||
},
|
|
||||||
Class: plugins.ClassCore,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: "2",
|
|
||||||
},
|
|
||||||
Class: plugins.ClassExternal,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
JSONData: plugins.JSONData{
|
|
||||||
ID: "3",
|
|
||||||
},
|
|
||||||
Class: plugins.ClassExternal,
|
|
||||||
},
|
|
||||||
}...)
|
|
||||||
|
|
||||||
s.dsService = &datafakes.FakeDataSourceService{
|
|
||||||
DataSources: []*datasources.DataSource{
|
|
||||||
{UID: "1", Type: "1"},
|
|
||||||
{UID: "2", Type: "2"},
|
|
||||||
{UID: "3", Type: "3"},
|
|
||||||
{UID: "4", Type: "4"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshot *cloudmigration.CloudMigrationSnapshot
|
|
||||||
hasFourResources := func() bool {
|
|
||||||
// Retrieve the snapshot with results
|
|
||||||
var err error
|
|
||||||
snapshot, err = s.GetSnapshot(ctxWithSignedInUser(), cloudmigration.GetSnapshotsQuery{
|
|
||||||
SnapshotUID: snapshotUid,
|
|
||||||
SessionUID: sess.UID,
|
|
||||||
ResultPage: 1,
|
|
||||||
ResultLimit: 10,
|
|
||||||
})
|
|
||||||
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(snapshot.Resources) == 4
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Eventually(t, hasFourResources, time.Second, 10*time.Millisecond)
|
|
||||||
|
|
||||||
findRef := func(id string) *cloudmigration.CloudMigrationResource {
|
|
||||||
for _, r := range snapshot.Resources {
|
|
||||||
if r.RefID == id {
|
|
||||||
return &r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldBeUnaltered := findRef("1")
|
|
||||||
assert.Equal(t, cloudmigration.ItemStatusOK, shouldBeUnaltered.Status)
|
|
||||||
assert.Empty(t, shouldBeUnaltered.Error)
|
|
||||||
|
|
||||||
shouldBeAltered := findRef("2")
|
|
||||||
assert.Equal(t, cloudmigration.ItemStatusWarning, shouldBeAltered.Status)
|
|
||||||
assert.Equal(t, shouldBeAltered.Error, "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.")
|
|
||||||
|
|
||||||
shouldHaveOriginalError := findRef("3")
|
|
||||||
assert.Equal(t, cloudmigration.ItemStatusError, shouldHaveOriginalError.Status)
|
|
||||||
assert.Equal(t, shouldHaveOriginalError.Error, "please don't overwrite me")
|
|
||||||
|
|
||||||
uninstalledAltered := findRef("4")
|
|
||||||
assert.Equal(t, cloudmigration.ItemStatusWarning, uninstalledAltered.Status)
|
|
||||||
assert.Equal(t, uninstalledAltered.Error, "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteSession(t *testing.T) {
|
func TestDeleteSession(t *testing.T) {
|
||||||
s := setUpServiceTest(t, false).(*Service)
|
s := setUpServiceTest(t, false).(*Service)
|
||||||
user := &user.SignedInUser{UserUID: "user123"}
|
user := &user.SignedInUser{UserUID: "user123"}
|
||||||
@ -817,13 +676,135 @@ func TestGetLibraryElementsCommands(t *testing.T) {
|
|||||||
require.Equal(t, createLibraryElementCmd.UID, cmds[0].UID)
|
require.Equal(t, createLibraryElementCmd.UID, cmds[0].UID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ctxWithSignedInUser() context.Context {
|
// NOTE: this should be on the plugin object
|
||||||
c := &contextmodel.ReqContext{
|
func TestIsPublicSignatureType(t *testing.T) {
|
||||||
SignedInUser: &user.SignedInUser{OrgID: 1},
|
testcases := []struct {
|
||||||
|
signature plugins.SignatureType
|
||||||
|
expectedPublic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
signature: plugins.SignatureTypeCommunity,
|
||||||
|
expectedPublic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: plugins.SignatureTypeCommercial,
|
||||||
|
expectedPublic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: plugins.SignatureTypeGrafana,
|
||||||
|
expectedPublic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: plugins.SignatureTypePrivate,
|
||||||
|
expectedPublic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: plugins.SignatureTypePrivateGlob,
|
||||||
|
expectedPublic: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
k := ctxkey.Key{}
|
|
||||||
ctx := context.WithValue(context.Background(), k, c)
|
for _, testcase := range testcases {
|
||||||
return ctx
|
resPublic := IsPublicSignatureType(testcase.signature)
|
||||||
|
require.Equal(t, resPublic, testcase.expectedPublic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPlugins(t *testing.T) {
|
||||||
|
s := setUpServiceTest(t, false).(*Service)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
user := &user.SignedInUser{OrgID: 1}
|
||||||
|
|
||||||
|
s.pluginStore = pluginstore.NewFakePluginStore([]pluginstore.Plugin{
|
||||||
|
{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "plugin-core",
|
||||||
|
Type: plugins.TypeDataSource,
|
||||||
|
},
|
||||||
|
Class: plugins.ClassCore,
|
||||||
|
Signature: plugins.SignatureStatusValid,
|
||||||
|
SignatureType: plugins.SignatureTypeGrafana,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "plugin-external-valid-grafana",
|
||||||
|
Type: plugins.TypeDataSource,
|
||||||
|
AutoEnabled: false,
|
||||||
|
},
|
||||||
|
Class: plugins.ClassExternal,
|
||||||
|
Signature: plugins.SignatureStatusValid,
|
||||||
|
SignatureType: plugins.SignatureTypeGrafana,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "plugin-external-valid-commercial",
|
||||||
|
Type: plugins.TypePanel,
|
||||||
|
},
|
||||||
|
Class: plugins.ClassExternal,
|
||||||
|
Signature: plugins.SignatureStatusValid,
|
||||||
|
SignatureType: plugins.SignatureTypeCommercial,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "plugin-external-valid-community",
|
||||||
|
Type: plugins.TypePanel,
|
||||||
|
},
|
||||||
|
Class: plugins.ClassExternal,
|
||||||
|
Signature: plugins.SignatureStatusValid,
|
||||||
|
SignatureType: plugins.SignatureTypeCommunity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "plugin-external-invalid",
|
||||||
|
Type: plugins.TypePanel,
|
||||||
|
},
|
||||||
|
Class: plugins.ClassExternal,
|
||||||
|
Signature: plugins.SignatureStatusInvalid,
|
||||||
|
SignatureType: plugins.SignatureTypeGrafana,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "plugin-external-unsigned",
|
||||||
|
Type: plugins.TypePanel,
|
||||||
|
},
|
||||||
|
Class: plugins.ClassExternal,
|
||||||
|
Signature: plugins.SignatureStatusUnsigned,
|
||||||
|
SignatureType: plugins.SignatureTypeGrafana,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "plugin-external-valid-private",
|
||||||
|
Type: plugins.TypeApp,
|
||||||
|
},
|
||||||
|
Class: plugins.ClassExternal,
|
||||||
|
Signature: plugins.SignatureStatusUnsigned,
|
||||||
|
SignatureType: plugins.SignatureTypePrivate,
|
||||||
|
},
|
||||||
|
}...)
|
||||||
|
|
||||||
|
s.pluginSettingsService = &pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
||||||
|
"plugin-external-valid-grafana": {ID: 0, OrgID: user.OrgID, PluginID: "plugin-external-valid-grafana", PluginVersion: "1.0.0", Enabled: true},
|
||||||
|
}}
|
||||||
|
|
||||||
|
plugins, err := s.getPlugins(ctx, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, plugins)
|
||||||
|
require.Len(t, plugins, 3)
|
||||||
|
|
||||||
|
expectedPluginIDs := []string{"plugin-external-valid-grafana", "plugin-external-valid-commercial", "plugin-external-valid-community"}
|
||||||
|
pluginsIDs := make([]string, 0)
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
// Special case of using the settings from the settings store
|
||||||
|
if plugin.ID == "plugin-external-valid-grafana" {
|
||||||
|
require.True(t, plugin.SettingCmd.Enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginsIDs = append(pluginsIDs, plugin.ID)
|
||||||
|
}
|
||||||
|
require.ElementsMatch(t, pluginsIDs, expectedPluginIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
type configOverrides func(c *setting.Cfg)
|
type configOverrides func(c *setting.Cfg)
|
||||||
@ -949,6 +930,8 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf
|
|||||||
dashboardService,
|
dashboardService,
|
||||||
mockFolder,
|
mockFolder,
|
||||||
&pluginstore.FakePluginStore{},
|
&pluginstore.FakePluginStore{},
|
||||||
|
&pluginsettings.FakePluginSettings{},
|
||||||
|
actest.FakeAccessControl{ExpectedEvaluate: true},
|
||||||
kvstore.ProvideService(sqlStore),
|
kvstore.ProvideService(sqlStore),
|
||||||
&libraryelementsfake.LibraryElementService{},
|
&libraryelementsfake.LibraryElementService{},
|
||||||
ng,
|
ng,
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
cryptoRand "crypto/rand"
|
cryptoRand "crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -14,12 +15,18 @@ import (
|
|||||||
snapshot "github.com/grafana/grafana-cloud-migration-snapshot/src"
|
snapshot "github.com/grafana/grafana-cloud-migration-snapshot/src"
|
||||||
"github.com/grafana/grafana-cloud-migration-snapshot/src/contracts"
|
"github.com/grafana/grafana-cloud-migration-snapshot/src/contracts"
|
||||||
"github.com/grafana/grafana-cloud-migration-snapshot/src/infra/crypto"
|
"github.com/grafana/grafana-cloud-migration-snapshot/src/infra/crypto"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
plugins "github.com/grafana/grafana/pkg/plugins"
|
||||||
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model"
|
libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/util/retryer"
|
"github.com/grafana/grafana/pkg/util/retryer"
|
||||||
"golang.org/x/crypto/nacl/box"
|
"golang.org/x/crypto/nacl/box"
|
||||||
@ -37,12 +44,20 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{
|
|||||||
cloudmigration.ContactPointType,
|
cloudmigration.ContactPointType,
|
||||||
cloudmigration.NotificationPolicyType,
|
cloudmigration.NotificationPolicyType,
|
||||||
cloudmigration.AlertRuleType,
|
cloudmigration.AlertRuleType,
|
||||||
|
cloudmigration.PluginDataType,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) {
|
func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) {
|
||||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getMigrationDataJSON")
|
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getMigrationDataJSON")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
plugins, err := s.getPlugins(ctx, signedInUser)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("Failed to get plugins", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Data sources
|
// Data sources
|
||||||
dataSources, err := s.getDataSourceCommands(ctx, signedInUser)
|
dataSources, err := s.getDataSourceCommands(ctx, signedInUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -100,10 +115,19 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S
|
|||||||
|
|
||||||
migrationDataSlice := make(
|
migrationDataSlice := make(
|
||||||
[]cloudmigration.MigrateDataRequestItem, 0,
|
[]cloudmigration.MigrateDataRequestItem, 0,
|
||||||
len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+
|
len(plugins)+len(dataSources)+len(dashs)+len(folders)+len(libraryElements)+
|
||||||
len(muteTimings)+len(notificationTemplates)+len(contactPoints)+len(alertRules),
|
len(muteTimings)+len(notificationTemplates)+len(contactPoints)+len(alertRules),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||||
|
Type: cloudmigration.PluginDataType,
|
||||||
|
RefID: plugin.ID,
|
||||||
|
Name: plugin.Name,
|
||||||
|
Data: plugin.SettingCmd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for _, ds := range dataSources {
|
for _, ds := range dataSources {
|
||||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||||
Type: cloudmigration.DatasourceDataType,
|
Type: cloudmigration.DatasourceDataType,
|
||||||
@ -356,6 +380,105 @@ func (s *Service) getLibraryElementsCommands(ctx context.Context, signedInUser *
|
|||||||
return cmds, nil
|
return cmds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PluginCmd struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
SettingCmd pluginsettings.UpdatePluginSettingCmd `json:"settingCmd"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPublicSignatureType returns true if plugin signature type is public
|
||||||
|
func IsPublicSignatureType(signatureType plugins.SignatureType) bool {
|
||||||
|
switch signatureType {
|
||||||
|
case plugins.SignatureTypeGrafana, plugins.SignatureTypeCommercial, plugins.SignatureTypeCommunity:
|
||||||
|
return true
|
||||||
|
case plugins.SignatureTypePrivate, plugins.SignatureTypePrivateGlob:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPlugins returns the json payloads required by the plugin creation API
|
||||||
|
func (s *Service) getPlugins(ctx context.Context, signedInUser *user.SignedInUser) ([]PluginCmd, error) {
|
||||||
|
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getPlugins")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
results := make([]PluginCmd, 0)
|
||||||
|
plugins := s.pluginStore.Plugins(ctx)
|
||||||
|
|
||||||
|
// Obtain plugins from gcom
|
||||||
|
requestID := tracing.TraceIDFromContext(ctx, false)
|
||||||
|
gcomPlugins, err := s.gcomService.GetPlugins(ctx, requestID)
|
||||||
|
if err != nil {
|
||||||
|
return results, fmt.Errorf("fetching gcom plugins: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissions for listing plugins, taken from plugins api
|
||||||
|
userIsOrgAdmin := signedInUser.HasRole(org.RoleAdmin)
|
||||||
|
hasAccess, _ := s.accessControl.Evaluate(ctx, signedInUser, ac.EvalAny(
|
||||||
|
ac.EvalPermission(datasources.ActionCreate),
|
||||||
|
ac.EvalPermission(pluginaccesscontrol.ActionInstall),
|
||||||
|
))
|
||||||
|
if !(userIsOrgAdmin || hasAccess) {
|
||||||
|
s.log.Info("user is not allowed to list non-core plugins", "UID", signedInUser.UserUID)
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
// filter plugins to keep only the ones allowed by gcom
|
||||||
|
if _, exists := gcomPlugins[plugin.ID]; !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter plugins to keep only non core, signed, with public signature type plugins
|
||||||
|
if plugin.IsCorePlugin() || !plugin.Signature.IsValid() || !IsPublicSignatureType(plugin.SignatureType) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// filter out dependent app plugins
|
||||||
|
if plugin.IncludedInAppID != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissions filtering, taken from plugins api
|
||||||
|
hasAccess, _ = s.accessControl.Evaluate(ctx, signedInUser, ac.EvalPermission(pluginaccesscontrol.ActionWrite, pluginaccesscontrol.ScopeProvider.GetResourceScope(plugin.ID)))
|
||||||
|
if !hasAccess {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginSettingCmd := pluginsettings.UpdatePluginSettingCmd{
|
||||||
|
Enabled: plugin.JSONData.AutoEnabled,
|
||||||
|
Pinned: plugin.Pinned,
|
||||||
|
PluginVersion: plugin.Info.Version,
|
||||||
|
PluginId: plugin.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// get plugin settings from db if they exist
|
||||||
|
ps, err := s.pluginSettingsService.GetPluginSettingByPluginID(ctx, &pluginsettings.GetByPluginIDArgs{
|
||||||
|
PluginID: plugin.ID,
|
||||||
|
OrgID: signedInUser.OrgID,
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) {
|
||||||
|
return nil, fmt.Errorf("failed to get plugin settings: %w", err)
|
||||||
|
} else if ps != nil {
|
||||||
|
pluginSettingCmd.Enabled = ps.Enabled
|
||||||
|
pluginSettingCmd.Pinned = ps.Pinned
|
||||||
|
pluginSettingCmd.JsonData = ps.JSONData
|
||||||
|
decryptedData, err := s.secretsService.DecryptJsonData(ctx, ps.SecureJSONData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decrypt secure json data: %w", err)
|
||||||
|
}
|
||||||
|
pluginSettingCmd.SecureJsonData = decryptedData
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, PluginCmd{
|
||||||
|
ID: plugin.ID,
|
||||||
|
Name: plugin.Name,
|
||||||
|
SettingCmd: pluginSettingCmd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
// asynchronous process for writing the snapshot to the filesystem and updating the snapshot status
|
// asynchronous process for writing the snapshot to the filesystem and updating the snapshot status
|
||||||
func (s *Service) buildSnapshot(ctx context.Context, signedInUser *user.SignedInUser, maxItemsPerPartition uint32, metadata []byte, snapshotMeta cloudmigration.CloudMigrationSnapshot) error {
|
func (s *Service) buildSnapshot(ctx context.Context, signedInUser *user.SignedInUser, maxItemsPerPartition uint32, metadata []byte, snapshotMeta cloudmigration.CloudMigrationSnapshot) error {
|
||||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.buildSnapshot")
|
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.buildSnapshot")
|
||||||
|
@ -92,6 +92,7 @@ const (
|
|||||||
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
|
NotificationPolicyType MigrateDataType = "NOTIFICATION_POLICY"
|
||||||
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
|
NotificationTemplateType MigrateDataType = "NOTIFICATION_TEMPLATE"
|
||||||
MuteTimingType MigrateDataType = "MUTE_TIMING"
|
MuteTimingType MigrateDataType = "MUTE_TIMING"
|
||||||
|
PluginDataType MigrateDataType = "PLUGIN"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ItemStatus string
|
type ItemStatus string
|
||||||
@ -116,7 +117,6 @@ const (
|
|||||||
ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT"
|
ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT"
|
||||||
ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE"
|
ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE"
|
||||||
ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR"
|
ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR"
|
||||||
ErrOnlyCoreDataSources ResourceErrorCode = "ONLY_CORE_DATA_SOURCES"
|
|
||||||
ErrGeneric ResourceErrorCode = "GENERIC_ERROR"
|
ErrGeneric ResourceErrorCode = "GENERIC_ERROR"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5637,7 +5637,6 @@
|
|||||||
"RESOURCE_CONFLICT",
|
"RESOURCE_CONFLICT",
|
||||||
"UNEXPECTED_STATUS_CODE",
|
"UNEXPECTED_STATUS_CODE",
|
||||||
"INTERNAL_SERVICE_ERROR",
|
"INTERNAL_SERVICE_ERROR",
|
||||||
"ONLY_CORE_DATA_SOURCES",
|
|
||||||
"GENERIC_ERROR"
|
"GENERIC_ERROR"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -5674,7 +5673,8 @@
|
|||||||
"CONTACT_POINT",
|
"CONTACT_POINT",
|
||||||
"NOTIFICATION_POLICY",
|
"NOTIFICATION_POLICY",
|
||||||
"NOTIFICATION_TEMPLATE",
|
"NOTIFICATION_TEMPLATE",
|
||||||
"MUTE_TIMING"
|
"MUTE_TIMING",
|
||||||
|
"PLUGIN"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17137,7 +17137,6 @@
|
|||||||
"RESOURCE_CONFLICT",
|
"RESOURCE_CONFLICT",
|
||||||
"UNEXPECTED_STATUS_CODE",
|
"UNEXPECTED_STATUS_CODE",
|
||||||
"INTERNAL_SERVICE_ERROR",
|
"INTERNAL_SERVICE_ERROR",
|
||||||
"ONLY_CORE_DATA_SOURCES",
|
|
||||||
"GENERIC_ERROR"
|
"GENERIC_ERROR"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -17174,7 +17173,8 @@
|
|||||||
"CONTACT_POINT",
|
"CONTACT_POINT",
|
||||||
"NOTIFICATION_POLICY",
|
"NOTIFICATION_POLICY",
|
||||||
"NOTIFICATION_TEMPLATE",
|
"NOTIFICATION_TEMPLATE",
|
||||||
"MUTE_TIMING"
|
"MUTE_TIMING",
|
||||||
|
"PLUGIN"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,18 @@ export default function MigrateToCloud() {
|
|||||||
to learn more about this feature!
|
to learn more about this feature!
|
||||||
</Trans>
|
</Trans>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
title={t('migrate-to-cloud.public-preview.title-plugins', 'Migration of plugins')}
|
||||||
|
buttonContent={''}
|
||||||
|
severity={'info'}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="migrate-to-cloud.public-preview.message-plugins">
|
||||||
|
Only Community and Commercial signed plugins are eligible for migration. Their latest version will be
|
||||||
|
installed in the cloud instance, please upgrade your plugins before starting the migration process.
|
||||||
|
</Trans>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
{config.cloudMigrationIsTarget ? <CloudPage /> : <OnPremPage />}
|
{config.cloudMigrationIsTarget ? <CloudPage /> : <OnPremPage />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
@ -182,7 +182,6 @@ export type MigrateDataResponseItemDto = {
|
|||||||
| 'RESOURCE_CONFLICT'
|
| 'RESOURCE_CONFLICT'
|
||||||
| 'UNEXPECTED_STATUS_CODE'
|
| 'UNEXPECTED_STATUS_CODE'
|
||||||
| 'INTERNAL_SERVICE_ERROR'
|
| 'INTERNAL_SERVICE_ERROR'
|
||||||
| 'ONLY_CORE_DATA_SOURCES'
|
|
||||||
| 'GENERIC_ERROR';
|
| 'GENERIC_ERROR';
|
||||||
message?: string;
|
message?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -198,7 +197,8 @@ export type MigrateDataResponseItemDto = {
|
|||||||
| 'CONTACT_POINT'
|
| 'CONTACT_POINT'
|
||||||
| 'NOTIFICATION_POLICY'
|
| 'NOTIFICATION_POLICY'
|
||||||
| 'NOTIFICATION_TEMPLATE'
|
| 'NOTIFICATION_TEMPLATE'
|
||||||
| 'MUTE_TIMING';
|
| 'MUTE_TIMING'
|
||||||
|
| 'PLUGIN';
|
||||||
};
|
};
|
||||||
export type SnapshotResourceStats = {
|
export type SnapshotResourceStats = {
|
||||||
statuses?: {
|
statuses?: {
|
||||||
|
@ -1,55 +1,75 @@
|
|||||||
export * from './endpoints.gen';
|
export * from './endpoints.gen';
|
||||||
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
|
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
|
||||||
|
|
||||||
|
import { getLocalPlugins } from 'app/features/plugins/admin/api';
|
||||||
|
import { LocalPlugin } from 'app/features/plugins/admin/types';
|
||||||
|
|
||||||
import { generatedAPI } from './endpoints.gen';
|
import { generatedAPI } from './endpoints.gen';
|
||||||
|
|
||||||
export const cloudMigrationAPI = generatedAPI.enhanceEndpoints({
|
export const cloudMigrationAPI = generatedAPI
|
||||||
addTagTypes: ['cloud-migration-token', 'cloud-migration-session', 'cloud-migration-snapshot'],
|
.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
// Manually written because the Swagger specifications for the plugins endpoint do not exist
|
||||||
|
getLocalPluginList: build.query<LocalPlugin[], void>({
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const list = await getLocalPlugins();
|
||||||
|
return { data: list };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.enhanceEndpoints({
|
||||||
|
addTagTypes: ['cloud-migration-token', 'cloud-migration-session', 'cloud-migration-snapshot'],
|
||||||
|
|
||||||
endpoints: {
|
endpoints: {
|
||||||
// Cloud-side - create token
|
// Cloud-side - create token
|
||||||
getCloudMigrationToken: {
|
getCloudMigrationToken: {
|
||||||
providesTags: ['cloud-migration-token'],
|
providesTags: ['cloud-migration-token'],
|
||||||
},
|
},
|
||||||
createCloudMigrationToken: {
|
createCloudMigrationToken: {
|
||||||
invalidatesTags: ['cloud-migration-token'],
|
invalidatesTags: ['cloud-migration-token'],
|
||||||
},
|
},
|
||||||
deleteCloudMigrationToken: {
|
deleteCloudMigrationToken: {
|
||||||
invalidatesTags: ['cloud-migration-token'],
|
invalidatesTags: ['cloud-migration-token'],
|
||||||
},
|
},
|
||||||
|
|
||||||
// On-prem session management (entering token)
|
// On-prem session management (entering token)
|
||||||
getSessionList: {
|
getSessionList: {
|
||||||
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
|
providesTags: ['cloud-migration-session'] /* should this be a -list? */,
|
||||||
},
|
},
|
||||||
getSession: {
|
getSession: {
|
||||||
providesTags: ['cloud-migration-session'],
|
providesTags: ['cloud-migration-session'],
|
||||||
},
|
},
|
||||||
createSession: {
|
createSession: {
|
||||||
invalidatesTags: ['cloud-migration-session'],
|
invalidatesTags: ['cloud-migration-session'],
|
||||||
},
|
},
|
||||||
deleteSession: {
|
deleteSession: {
|
||||||
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
|
invalidatesTags: ['cloud-migration-session', 'cloud-migration-snapshot'],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Snapshot management
|
// Snapshot management
|
||||||
getShapshotList: {
|
getShapshotList: {
|
||||||
providesTags: ['cloud-migration-snapshot'],
|
providesTags: ['cloud-migration-snapshot'],
|
||||||
},
|
},
|
||||||
getSnapshot: {
|
getSnapshot: {
|
||||||
providesTags: ['cloud-migration-snapshot'],
|
providesTags: ['cloud-migration-snapshot'],
|
||||||
},
|
},
|
||||||
createSnapshot: {
|
createSnapshot: {
|
||||||
invalidatesTags: ['cloud-migration-snapshot'],
|
invalidatesTags: ['cloud-migration-snapshot'],
|
||||||
},
|
},
|
||||||
uploadSnapshot: {
|
uploadSnapshot: {
|
||||||
invalidatesTags: ['cloud-migration-snapshot'],
|
invalidatesTags: ['cloud-migration-snapshot'],
|
||||||
},
|
},
|
||||||
|
|
||||||
getDashboardByUid: suppressErrorsOnQuery,
|
getDashboardByUid: suppressErrorsOnQuery,
|
||||||
getLibraryElementByUid: suppressErrorsOnQuery,
|
getLibraryElementByUid: suppressErrorsOnQuery,
|
||||||
},
|
getLocalPluginList: suppressErrorsOnQuery,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes extends string, ResultType>(
|
function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes extends string, ResultType>(
|
||||||
endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType>
|
endpoint: EndpointDefinition<QueryArg, BaseQuery, TagTypes, ResultType>
|
||||||
@ -65,3 +85,5 @@ function suppressErrorsOnQuery<QueryArg, BaseQuery extends BaseQueryFn, TagTypes
|
|||||||
return baseQuery;
|
return baseQuery;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const { useGetLocalPluginListQuery } = cloudMigrationAPI;
|
||||||
|
@ -9,6 +9,7 @@ import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
|
|||||||
import { Trans } from 'app/core/internationalization';
|
import { Trans } from 'app/core/internationalization';
|
||||||
import { useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
import { useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||||
|
|
||||||
|
import { LocalPlugin } from '../../plugins/admin/types';
|
||||||
import { useGetDashboardByUidQuery, useGetLibraryElementByUidQuery } from '../api';
|
import { useGetDashboardByUidQuery, useGetLibraryElementByUidQuery } from '../api';
|
||||||
|
|
||||||
import { ResourceTableItem } from './types';
|
import { ResourceTableItem } from './types';
|
||||||
@ -205,6 +206,7 @@ function BasicResourceInfo({ data }: { data: ResourceTableItem }) {
|
|||||||
function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
|
function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
|
||||||
const styles = useStyles2(getIconStyles);
|
const styles = useStyles2(getIconStyles);
|
||||||
const datasource = useDatasource(resource.type === 'DATASOURCE' ? resource.refId : undefined);
|
const datasource = useDatasource(resource.type === 'DATASOURCE' ? resource.refId : undefined);
|
||||||
|
const pluginLogo = usePluginLogo(resource.type === 'PLUGIN' ? resource.plugin : undefined);
|
||||||
|
|
||||||
switch (resource.type) {
|
switch (resource.type) {
|
||||||
case 'DASHBOARD':
|
case 'DASHBOARD':
|
||||||
@ -229,6 +231,11 @@ function ResourceIcon({ resource }: { resource: ResourceTableItem }) {
|
|||||||
return <Icon size="xl" name="bell" />;
|
return <Icon size="xl" name="bell" />;
|
||||||
case 'ALERT_RULE':
|
case 'ALERT_RULE':
|
||||||
return <Icon size="xl" name="bell" />;
|
return <Icon size="xl" name="bell" />;
|
||||||
|
case 'PLUGIN':
|
||||||
|
if (pluginLogo) {
|
||||||
|
return <img className={styles.icon} src={pluginLogo} alt="" />;
|
||||||
|
}
|
||||||
|
return <Icon size="xl" name="plug" />;
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -257,3 +264,14 @@ function useDatasource(datasourceUID: string | undefined): DataSourceInstanceSet
|
|||||||
|
|
||||||
return datasource;
|
return datasource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function usePluginLogo(plugin: LocalPlugin | undefined): string | undefined {
|
||||||
|
const logos = useMemo(() => {
|
||||||
|
if (!plugin) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return plugin?.info?.logos;
|
||||||
|
}, [plugin]);
|
||||||
|
|
||||||
|
return logos?.small;
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
useGetShapshotListQuery,
|
useGetShapshotListQuery,
|
||||||
useGetSnapshotQuery,
|
useGetSnapshotQuery,
|
||||||
useUploadSnapshotMutation,
|
useUploadSnapshotMutation,
|
||||||
|
useGetLocalPluginListQuery,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { AlertWithTraceID } from '../shared/AlertWithTraceID';
|
import { AlertWithTraceID } from '../shared/AlertWithTraceID';
|
||||||
|
|
||||||
@ -122,6 +123,8 @@ export const Page = () => {
|
|||||||
const [performCancelSnapshot, cancelSnapshotResult] = useCancelSnapshotMutation();
|
const [performCancelSnapshot, cancelSnapshotResult] = useCancelSnapshotMutation();
|
||||||
const [performDisconnect, disconnectResult] = useDeleteSessionMutation();
|
const [performDisconnect, disconnectResult] = useDeleteSessionMutation();
|
||||||
|
|
||||||
|
const { currentData: localPlugins = [] } = useGetLocalPluginListQuery();
|
||||||
|
|
||||||
useNotifySuccessful(snapshot.data);
|
useNotifySuccessful(snapshot.data);
|
||||||
|
|
||||||
const sessionUid = session.data?.uid;
|
const sessionUid = session.data?.uid;
|
||||||
@ -240,6 +243,7 @@ export const Page = () => {
|
|||||||
<Stack gap={4} direction="column">
|
<Stack gap={4} direction="column">
|
||||||
<ResourcesTable
|
<ResourcesTable
|
||||||
resources={snapshot.data.results}
|
resources={snapshot.data.results}
|
||||||
|
localPlugins={localPlugins}
|
||||||
onChangePage={setPage}
|
onChangePage={setPage}
|
||||||
numberOfPages={Math.ceil((snapshot?.data?.stats?.total || 0) / PAGE_SIZE)}
|
numberOfPages={Math.ceil((snapshot?.data?.stats?.total || 0) / PAGE_SIZE)}
|
||||||
page={page}
|
page={page}
|
||||||
|
@ -53,11 +53,6 @@ function getTMessage(errorCode: MigrateDataResponseItemDto['errorCode']): string
|
|||||||
'migrate-to-cloud.resource-details.error-messages.resource-conflict',
|
'migrate-to-cloud.resource-details.error-messages.resource-conflict',
|
||||||
'There is a resource conflict with the target instance. Please check the Grafana server logs for more details.'
|
'There is a resource conflict with the target instance. Please check the Grafana server logs for more details.'
|
||||||
);
|
);
|
||||||
case 'ONLY_CORE_DATA_SOURCES':
|
|
||||||
return t(
|
|
||||||
'migrate-to-cloud.resource-details.error-messages.only-core-data-sources',
|
|
||||||
'Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.'
|
|
||||||
);
|
|
||||||
case 'UNEXPECTED_STATUS_CODE':
|
case 'UNEXPECTED_STATUS_CODE':
|
||||||
return t(
|
return t(
|
||||||
'migrate-to-cloud.resource-details.error-messages.unexpected-error',
|
'migrate-to-cloud.resource-details.error-messages.unexpected-error',
|
||||||
|
@ -19,7 +19,13 @@ setBackendSrv(backendSrv);
|
|||||||
function render(props: Partial<ResourcesTableProps>) {
|
function render(props: Partial<ResourcesTableProps>) {
|
||||||
rtlRender(
|
rtlRender(
|
||||||
<TestProvider>
|
<TestProvider>
|
||||||
<ResourcesTable onChangePage={() => {}} numberOfPages={10} page={0} resources={props.resources || []} />
|
<ResourcesTable
|
||||||
|
onChangePage={() => {}}
|
||||||
|
numberOfPages={10}
|
||||||
|
page={0}
|
||||||
|
resources={props.resources || []}
|
||||||
|
localPlugins={[]}
|
||||||
|
/>
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { InteractiveTable, Pagination, Stack } from '@grafana/ui';
|
import { InteractiveTable, Pagination, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { LocalPlugin } from '../../plugins/admin/types';
|
||||||
import { MigrateDataResponseItemDto } from '../api';
|
import { MigrateDataResponseItemDto } from '../api';
|
||||||
|
|
||||||
import { NameCell } from './NameCell';
|
import { NameCell } from './NameCell';
|
||||||
@ -12,6 +13,7 @@ import { ResourceTableItem } from './types';
|
|||||||
|
|
||||||
export interface ResourcesTableProps {
|
export interface ResourcesTableProps {
|
||||||
resources: MigrateDataResponseItemDto[];
|
resources: MigrateDataResponseItemDto[];
|
||||||
|
localPlugins: LocalPlugin[];
|
||||||
page: number;
|
page: number;
|
||||||
numberOfPages: number;
|
numberOfPages: number;
|
||||||
onChangePage: (page: number) => void;
|
onChangePage: (page: number) => void;
|
||||||
@ -23,7 +25,13 @@ const columns = [
|
|||||||
{ id: 'status', header: 'Status', cell: StatusCell },
|
{ id: 'status', header: 'Status', cell: StatusCell },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, page = 1 }: ResourcesTableProps) {
|
export function ResourcesTable({
|
||||||
|
resources,
|
||||||
|
localPlugins,
|
||||||
|
numberOfPages = 0,
|
||||||
|
onChangePage,
|
||||||
|
page = 1,
|
||||||
|
}: ResourcesTableProps) {
|
||||||
const [focusedResource, setfocusedResource] = useState<ResourceTableItem | undefined>();
|
const [focusedResource, setfocusedResource] = useState<ResourceTableItem | undefined>();
|
||||||
|
|
||||||
const handleShowDetailsModal = useCallback((resource: ResourceTableItem) => {
|
const handleShowDetailsModal = useCallback((resource: ResourceTableItem) => {
|
||||||
@ -31,8 +39,16 @@ export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, pag
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
return resources.map((r) => ({ ...r, showDetails: handleShowDetailsModal }));
|
return resources.map((r) => {
|
||||||
}, [resources, handleShowDetailsModal]);
|
const plugin = getPlugin(r, localPlugins);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
showDetails: handleShowDetailsModal,
|
||||||
|
plugin: plugin,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [resources, handleShowDetailsModal, localPlugins]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -46,3 +62,14 @@ export function ResourcesTable({ resources, numberOfPages = 0, onChangePage, pag
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlugin(
|
||||||
|
r: MigrateDataResponseItemDto | undefined,
|
||||||
|
plugins: LocalPlugin[] | undefined
|
||||||
|
): LocalPlugin | undefined {
|
||||||
|
if (!r || !plugins || r.type !== 'PLUGIN') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins.find((plugin) => plugin.id === r.refId);
|
||||||
|
}
|
||||||
|
@ -23,6 +23,8 @@ export function prettyTypeName(type: ResourceTableItem['type']) {
|
|||||||
return t('migrate-to-cloud.resource-type.notification_policy', 'Notification Policy');
|
return t('migrate-to-cloud.resource-type.notification_policy', 'Notification Policy');
|
||||||
case 'ALERT_RULE':
|
case 'ALERT_RULE':
|
||||||
return t('migrate-to-cloud.resource-type.alert_rule', 'Alert Rule');
|
return t('migrate-to-cloud.resource-type.alert_rule', 'Alert Rule');
|
||||||
|
case 'PLUGIN':
|
||||||
|
return t('migrate-to-cloud.resource-type.plugin', 'Plugin');
|
||||||
default:
|
default:
|
||||||
return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
|
return t('migrate-to-cloud.resource-type.unknown', 'Unknown');
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import { LocalPlugin } from '../../plugins/admin/types';
|
||||||
import { MigrateDataResponseItemDto } from '../api';
|
import { MigrateDataResponseItemDto } from '../api';
|
||||||
|
|
||||||
export interface ResourceTableItem extends MigrateDataResponseItemDto {
|
export interface ResourceTableItem extends MigrateDataResponseItemDto {
|
||||||
showDetails: (resource: ResourceTableItem) => void;
|
showDetails: (resource: ResourceTableItem) => void;
|
||||||
|
plugin: LocalPlugin | undefined;
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,8 @@ function getTranslatedMessage(snapshot: GetSnapshotResponseDto) {
|
|||||||
types.push(t('migrate-to-cloud.migrated-counts.notification_policies', 'notification policies'));
|
types.push(t('migrate-to-cloud.migrated-counts.notification_policies', 'notification policies'));
|
||||||
} else if (type === 'ALERT_RULE') {
|
} else if (type === 'ALERT_RULE') {
|
||||||
types.push(t('migrate-to-cloud.migrated-counts.alert_rules', 'alert rules'));
|
types.push(t('migrate-to-cloud.migrated-counts.alert_rules', 'alert rules'));
|
||||||
|
} else if (type === 'PLUGIN') {
|
||||||
|
types.push(t('migrate-to-cloud.migrated-counts.plugins', 'plugins'));
|
||||||
}
|
}
|
||||||
|
|
||||||
distinctItems += 1;
|
distinctItems += 1;
|
||||||
|
@ -1873,7 +1873,8 @@
|
|||||||
"library_elements": "library elements",
|
"library_elements": "library elements",
|
||||||
"mute_timings": "mute timings",
|
"mute_timings": "mute timings",
|
||||||
"notification_policies": "notification policies",
|
"notification_policies": "notification policies",
|
||||||
"notification_templates": "notification templates"
|
"notification_templates": "notification templates",
|
||||||
|
"plugins": "plugins"
|
||||||
},
|
},
|
||||||
"migration-token": {
|
"migration-token": {
|
||||||
"delete-button": "Delete token",
|
"delete-button": "Delete token",
|
||||||
@ -1924,7 +1925,9 @@
|
|||||||
"public-preview": {
|
"public-preview": {
|
||||||
"button-text": "Give feedback",
|
"button-text": "Give feedback",
|
||||||
"message": "No SLAs are available yet. <2>Visit our docs</2> to learn more about this feature!",
|
"message": "No SLAs are available yet. <2>Visit our docs</2> to learn more about this feature!",
|
||||||
"title": "Migrate to Grafana Cloud is in public preview"
|
"message-plugins": "Only Community and Commercial signed plugins are eligible for migration. Their latest version will be installed in the cloud instance, please upgrade your plugins before starting the migration process.",
|
||||||
|
"title": "Migrate to Grafana Cloud is in public preview",
|
||||||
|
"title-plugins": "Migration of plugins"
|
||||||
},
|
},
|
||||||
"resource-details": {
|
"resource-details": {
|
||||||
"dismiss-button": "OK",
|
"dismiss-button": "OK",
|
||||||
@ -1937,7 +1940,6 @@
|
|||||||
"generic-error": "There has been an error while migrating. Please check the cloud migration logs for more information.",
|
"generic-error": "There has been an error while migrating. Please check the cloud migration logs for more information.",
|
||||||
"internal-service-error": "There has been an error while migrating. Please check the Grafana server logs for more details.",
|
"internal-service-error": "There has been an error while migrating. Please check the Grafana server logs for more details.",
|
||||||
"library-element-name-conflict": "There is a library element with the same name in the target instance. Rename one of them and try again.",
|
"library-element-name-conflict": "There is a library element with the same name in the target instance. Rename one of them and try again.",
|
||||||
"only-core-data-sources": "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.",
|
|
||||||
"resource-conflict": "There is a resource conflict with the target instance. Please check the Grafana server logs for more details.",
|
"resource-conflict": "There is a resource conflict with the target instance. Please check the Grafana server logs for more details.",
|
||||||
"unexpected-error": "There has been an error while migrating. Please check the Grafana server logs for more details.",
|
"unexpected-error": "There has been an error while migrating. Please check the Grafana server logs for more details.",
|
||||||
"unsupported-data-type": "Migration of this data type is not currently supported."
|
"unsupported-data-type": "Migration of this data type is not currently supported."
|
||||||
@ -1976,6 +1978,7 @@
|
|||||||
"mute_timing": "Mute Timing",
|
"mute_timing": "Mute Timing",
|
||||||
"notification_policy": "Notification Policy",
|
"notification_policy": "Notification Policy",
|
||||||
"notification_template": "Notification Template",
|
"notification_template": "Notification Template",
|
||||||
|
"plugin": "Plugin",
|
||||||
"unknown": "Unknown"
|
"unknown": "Unknown"
|
||||||
},
|
},
|
||||||
"summary": {
|
"summary": {
|
||||||
|
@ -1873,7 +1873,8 @@
|
|||||||
"library_elements": "ľįþřäřy ęľęmęʼnŧş",
|
"library_elements": "ľįþřäřy ęľęmęʼnŧş",
|
||||||
"mute_timings": "mūŧę ŧįmįʼnģş",
|
"mute_timings": "mūŧę ŧįmįʼnģş",
|
||||||
"notification_policies": "ʼnőŧįƒįčäŧįőʼn pőľįčįęş",
|
"notification_policies": "ʼnőŧįƒįčäŧįőʼn pőľįčįęş",
|
||||||
"notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş"
|
"notification_templates": "ʼnőŧįƒįčäŧįőʼn ŧęmpľäŧęş",
|
||||||
|
"plugins": "pľūģįʼnş"
|
||||||
},
|
},
|
||||||
"migration-token": {
|
"migration-token": {
|
||||||
"delete-button": "Đęľęŧę ŧőĸęʼn",
|
"delete-button": "Đęľęŧę ŧőĸęʼn",
|
||||||
@ -1924,7 +1925,9 @@
|
|||||||
"public-preview": {
|
"public-preview": {
|
||||||
"button-text": "Ğįvę ƒęęđþäčĸ",
|
"button-text": "Ğįvę ƒęęđþäčĸ",
|
||||||
"message": "Ńő ŜĿÅş äřę äväįľäþľę yęŧ. <2>Vįşįŧ őūř đőčş</2> ŧő ľęäřʼn mőřę äþőūŧ ŧĥįş ƒęäŧūřę!",
|
"message": "Ńő ŜĿÅş äřę äväįľäþľę yęŧ. <2>Vįşįŧ őūř đőčş</2> ŧő ľęäřʼn mőřę äþőūŧ ŧĥįş ƒęäŧūřę!",
|
||||||
"title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ"
|
"message-plugins": "Øʼnľy Cőmmūʼnįŧy äʼnđ Cőmmęřčįäľ şįģʼnęđ pľūģįʼnş äřę ęľįģįþľę ƒőř mįģřäŧįőʼn. Ŧĥęįř ľäŧęşŧ vęřşįőʼn ŵįľľ þę įʼnşŧäľľęđ įʼn ŧĥę čľőūđ įʼnşŧäʼnčę, pľęäşę ūpģřäđę yőūř pľūģįʼnş þęƒőřę şŧäřŧįʼnģ ŧĥę mįģřäŧįőʼn přőčęşş.",
|
||||||
|
"title": "Mįģřäŧę ŧő Ğřäƒäʼnä Cľőūđ įş įʼn pūþľįč přęvįęŵ",
|
||||||
|
"title-plugins": "Mįģřäŧįőʼn őƒ pľūģįʼnş"
|
||||||
},
|
},
|
||||||
"resource-details": {
|
"resource-details": {
|
||||||
"dismiss-button": "ØĶ",
|
"dismiss-button": "ØĶ",
|
||||||
@ -1937,7 +1940,6 @@
|
|||||||
"generic-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę čľőūđ mįģřäŧįőʼn ľőģş ƒőř mőřę įʼnƒőřmäŧįőʼn.",
|
"generic-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę čľőūđ mįģřäŧįőʼn ľőģş ƒőř mőřę įʼnƒőřmäŧįőʼn.",
|
||||||
"internal-service-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
"internal-service-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||||
"library-element-name-conflict": "Ŧĥęřę įş ä ľįþřäřy ęľęmęʼnŧ ŵįŧĥ ŧĥę şämę ʼnämę įʼn ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Ŗęʼnämę őʼnę őƒ ŧĥęm äʼnđ ŧřy äģäįʼn.",
|
"library-element-name-conflict": "Ŧĥęřę įş ä ľįþřäřy ęľęmęʼnŧ ŵįŧĥ ŧĥę şämę ʼnämę įʼn ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Ŗęʼnämę őʼnę őƒ ŧĥęm äʼnđ ŧřy äģäįʼn.",
|
||||||
"only-core-data-sources": "Øʼnľy čőřę đäŧä şőūřčęş äřę şūppőřŧęđ. Pľęäşę ęʼnşūřę ŧĥę pľūģįʼn įş įʼnşŧäľľęđ őʼn ŧĥę čľőūđ şŧäčĸ.",
|
|
||||||
"resource-conflict": "Ŧĥęřę įş ä řęşőūřčę čőʼnƒľįčŧ ŵįŧĥ ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
"resource-conflict": "Ŧĥęřę įş ä řęşőūřčę čőʼnƒľįčŧ ŵįŧĥ ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||||
"unexpected-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
"unexpected-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||||
"unsupported-data-type": "Mįģřäŧįőʼn őƒ ŧĥįş đäŧä ŧypę įş ʼnőŧ čūřřęʼnŧľy şūppőřŧęđ."
|
"unsupported-data-type": "Mįģřäŧįőʼn őƒ ŧĥįş đäŧä ŧypę įş ʼnőŧ čūřřęʼnŧľy şūppőřŧęđ."
|
||||||
@ -1976,6 +1978,7 @@
|
|||||||
"mute_timing": "Mūŧę Ŧįmįʼnģ",
|
"mute_timing": "Mūŧę Ŧįmįʼnģ",
|
||||||
"notification_policy": "Ńőŧįƒįčäŧįőʼn Pőľįčy",
|
"notification_policy": "Ńőŧįƒįčäŧįőʼn Pőľįčy",
|
||||||
"notification_template": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧę",
|
"notification_template": "Ńőŧįƒįčäŧįőʼn Ŧęmpľäŧę",
|
||||||
|
"plugin": "Pľūģįʼn",
|
||||||
"unknown": "Ůʼnĸʼnőŵʼn"
|
"unknown": "Ůʼnĸʼnőŵʼn"
|
||||||
},
|
},
|
||||||
"summary": {
|
"summary": {
|
||||||
|
@ -7205,7 +7205,6 @@
|
|||||||
"RESOURCE_CONFLICT",
|
"RESOURCE_CONFLICT",
|
||||||
"UNEXPECTED_STATUS_CODE",
|
"UNEXPECTED_STATUS_CODE",
|
||||||
"INTERNAL_SERVICE_ERROR",
|
"INTERNAL_SERVICE_ERROR",
|
||||||
"ONLY_CORE_DATA_SOURCES",
|
|
||||||
"GENERIC_ERROR"
|
"GENERIC_ERROR"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -7242,7 +7241,8 @@
|
|||||||
"CONTACT_POINT",
|
"CONTACT_POINT",
|
||||||
"NOTIFICATION_POLICY",
|
"NOTIFICATION_POLICY",
|
||||||
"NOTIFICATION_TEMPLATE",
|
"NOTIFICATION_TEMPLATE",
|
||||||
"MUTE_TIMING"
|
"MUTE_TIMING",
|
||||||
|
"PLUGIN"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user