CloudMigrations: Create resource dependency map to validate snapshot request (#102594)
* CloudMigrations: Create resource dependency map to validate snapshot request * CloudMigrations: Validate resource types dependencies in create snapshot request * CloudMigrations: Update service interface to pass parsed resource types for creation * CloudMigrations: Conditionally append resource to snapshot if enabled * CloudMigrations: Add /cloudmigration/resources/dependencies endpoint * CloudMigrations: Properly filter dashboards and folders from snapshot
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -14,8 +16,6 @@ import (
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
type CloudMigrationAPI struct {
|
||||
@@ -23,6 +23,7 @@ type CloudMigrationAPI struct {
|
||||
routeRegister routing.RouteRegister
|
||||
log log.Logger
|
||||
tracer tracing.Tracer
|
||||
resourceDependencyMap cloudmigration.DependencyMap
|
||||
}
|
||||
|
||||
func RegisterApi(
|
||||
@@ -30,12 +31,14 @@ func RegisterApi(
|
||||
cms cloudmigration.Service,
|
||||
tracer tracing.Tracer,
|
||||
acHandler accesscontrol.AccessControl,
|
||||
resourceDependencyMap cloudmigration.DependencyMap,
|
||||
) *CloudMigrationAPI {
|
||||
api := &CloudMigrationAPI{
|
||||
log: log.New("cloudmigrations.api"),
|
||||
routeRegister: rr,
|
||||
cloudMigrationService: cms,
|
||||
tracer: tracer,
|
||||
resourceDependencyMap: resourceDependencyMap,
|
||||
}
|
||||
api.registerEndpoints(acHandler)
|
||||
return api
|
||||
@@ -63,6 +66,9 @@ func (cma *CloudMigrationAPI) registerEndpoints(acHandler accesscontrol.AccessCo
|
||||
cloudMigrationRoute.Get("/migration/:uid/snapshots", routing.Wrap(cma.GetSnapshotList))
|
||||
cloudMigrationRoute.Post("/migration/:uid/snapshot/:snapshotUid/upload", routing.Wrap(cma.UploadSnapshot))
|
||||
cloudMigrationRoute.Post("/migration/:uid/snapshot/:snapshotUid/cancel", routing.Wrap(cma.CancelSnapshot))
|
||||
|
||||
// resource dependency list
|
||||
cloudMigrationRoute.Get("/resources/dependencies", routing.Wrap(cma.GetResourceDependencies))
|
||||
}, authorize(cloudmigration.MigrationAssistantAccess))
|
||||
}
|
||||
|
||||
@@ -316,7 +322,6 @@ func (cma *CloudMigrationAPI) CreateSnapshot(c *contextmodel.ReqContext) respons
|
||||
defer span.End()
|
||||
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
|
||||
if err := util.ValidateUID(uid); err != nil {
|
||||
span.SetStatus(codes.Error, "invalid session uid")
|
||||
span.RecordError(err)
|
||||
@@ -324,7 +329,35 @@ func (cma *CloudMigrationAPI) CreateSnapshot(c *contextmodel.ReqContext) respons
|
||||
return response.ErrOrFallback(http.StatusBadRequest, "invalid session uid", err)
|
||||
}
|
||||
|
||||
ss, err := cma.cloudMigrationService.CreateSnapshot(ctx, c.SignedInUser, uid)
|
||||
var cmd CreateSnapshotRequestDTO
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
span.SetStatus(codes.Error, "invalid request body")
|
||||
span.RecordError(err)
|
||||
|
||||
return response.ErrOrFallback(http.StatusBadRequest, "invalid request body", err)
|
||||
}
|
||||
|
||||
if len(cmd.ResourceTypes) == 0 {
|
||||
return response.ErrOrFallback(http.StatusBadRequest, "at least one resource type is required", cloudmigration.ErrEmptyResourceTypes)
|
||||
}
|
||||
|
||||
rawResourceTypes := make([]cloudmigration.MigrateDataType, 0, len(cmd.ResourceTypes))
|
||||
for _, t := range cmd.ResourceTypes {
|
||||
rawResourceTypes = append(rawResourceTypes, cloudmigration.MigrateDataType(t))
|
||||
}
|
||||
|
||||
resourceTypes, err := cma.resourceDependencyMap.Parse(rawResourceTypes)
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, "invalid resource types")
|
||||
span.RecordError(err)
|
||||
|
||||
return response.ErrOrFallback(http.StatusBadRequest, "invalid resource types", err)
|
||||
}
|
||||
|
||||
ss, err := cma.cloudMigrationService.CreateSnapshot(ctx, c.SignedInUser, cloudmigration.CreateSnapshotCommand{
|
||||
SessionUID: uid,
|
||||
ResourceTypes: resourceTypes,
|
||||
})
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, "error creating snapshot")
|
||||
span.RecordError(err)
|
||||
@@ -606,3 +639,31 @@ func (cma *CloudMigrationAPI) CancelSnapshot(c *contextmodel.ReqContext) respons
|
||||
|
||||
return response.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// swagger:route GET /cloudmigration/resources/dependencies migrations getResourceDependencies
|
||||
//
|
||||
// Get the resource dependencies graph for the current set of migratable resources.
|
||||
//
|
||||
// Responses:
|
||||
// 200: resourceDependenciesResponse
|
||||
func (cma *CloudMigrationAPI) GetResourceDependencies(c *contextmodel.ReqContext) response.Response {
|
||||
_, span := cma.tracer.Start(c.Req.Context(), "MigrationAPI.GetResourceDependencies")
|
||||
defer span.End()
|
||||
|
||||
resourceDependencies := make([]ResourceDependencyDTO, 0, len(cma.resourceDependencyMap))
|
||||
for resourceType, dependencies := range cma.resourceDependencyMap {
|
||||
dependencyNames := make([]MigrateDataType, 0, len(dependencies))
|
||||
for _, dependency := range dependencies {
|
||||
dependencyNames = append(dependencyNames, MigrateDataType(dependency))
|
||||
}
|
||||
|
||||
resourceDependencies = append(resourceDependencies, ResourceDependencyDTO{
|
||||
ResourceType: MigrateDataType(resourceType),
|
||||
Dependencies: dependencyNames,
|
||||
})
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, ResourceDependenciesResponseDTO{
|
||||
ResourceDependencies: resourceDependencies,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
@@ -14,7 +16,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type TestCase struct {
|
||||
@@ -322,14 +323,43 @@ func TestCloudMigrationAPI_CreateSnapshot(t *testing.T) {
|
||||
desc: "returns 200 if the user has the right permissions",
|
||||
requestHttpMethod: http.MethodPost,
|
||||
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
|
||||
requestBody: `{"resourceTypes":["PLUGIN"]}`,
|
||||
user: userWithPermissions,
|
||||
expectedHttpResult: http.StatusOK,
|
||||
expectedBody: `{"uid":"fake_uid"}`,
|
||||
},
|
||||
{
|
||||
desc: "returns 400 if resource types are not provided",
|
||||
requestHttpMethod: http.MethodPost,
|
||||
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
|
||||
requestBody: `{}`,
|
||||
user: userWithPermissions,
|
||||
expectedHttpResult: http.StatusBadRequest,
|
||||
expectedBody: "",
|
||||
},
|
||||
{
|
||||
desc: "returns 400 if request body is not a valid json",
|
||||
requestHttpMethod: http.MethodPost,
|
||||
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
|
||||
requestBody: "asdf",
|
||||
user: userWithPermissions,
|
||||
expectedHttpResult: http.StatusBadRequest,
|
||||
expectedBody: "",
|
||||
},
|
||||
{
|
||||
desc: "returns 400 if resource types are invalid",
|
||||
requestHttpMethod: http.MethodPost,
|
||||
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
|
||||
requestBody: `{"resourceTypes":["INVALID"]}`,
|
||||
user: userWithPermissions,
|
||||
expectedHttpResult: http.StatusBadRequest,
|
||||
expectedBody: "",
|
||||
},
|
||||
{
|
||||
desc: "returns 403 if the user does not have the right permissions",
|
||||
requestHttpMethod: http.MethodPost,
|
||||
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
|
||||
requestBody: `{"resourceTypes":["PLUGIN"]}`,
|
||||
user: userWithoutPermissions,
|
||||
expectedHttpResult: http.StatusForbidden,
|
||||
expectedBody: "",
|
||||
@@ -338,6 +368,7 @@ func TestCloudMigrationAPI_CreateSnapshot(t *testing.T) {
|
||||
desc: "returns 500 if service returns an error",
|
||||
requestHttpMethod: http.MethodPost,
|
||||
requestUrl: "/api/cloudmigration/migration/1234/snapshot",
|
||||
requestBody: `{"resourceTypes":["PLUGIN"]}`,
|
||||
user: userWithPermissions,
|
||||
serviceReturnError: true,
|
||||
expectedHttpResult: http.StatusInternalServerError,
|
||||
@@ -347,6 +378,7 @@ func TestCloudMigrationAPI_CreateSnapshot(t *testing.T) {
|
||||
desc: "returns 400 if uid is invalid",
|
||||
requestHttpMethod: http.MethodPost,
|
||||
requestUrl: "/api/cloudmigration/migration/***/snapshot",
|
||||
requestBody: `{"resourceTypes":["PLUGIN"]}`,
|
||||
user: userWithPermissions,
|
||||
serviceReturnError: true,
|
||||
expectedHttpResult: http.StatusBadRequest,
|
||||
@@ -566,6 +598,31 @@ func TestCloudMigrationAPI_CancelSnapshot(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudMigrationAPI_GetResourceDependencies(t *testing.T) {
|
||||
tests := []TestCase{
|
||||
{
|
||||
desc: "returns 200 if the user has the right permissions",
|
||||
requestHttpMethod: http.MethodGet,
|
||||
requestUrl: "/api/cloudmigration/resources/dependencies",
|
||||
user: userWithPermissions,
|
||||
expectedHttpResult: http.StatusOK,
|
||||
expectedBody: `{"resourceDependencies":[{"resourceType":"PLUGIN","dependencies":[]}]}`,
|
||||
},
|
||||
{
|
||||
desc: "returns 403 if the user does not have the right permissions",
|
||||
requestHttpMethod: http.MethodGet,
|
||||
requestUrl: "/api/cloudmigration/resources/dependencies",
|
||||
user: userWithoutPermissions,
|
||||
expectedHttpResult: http.StatusForbidden,
|
||||
expectedBody: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, runSimpleApiTest(tt))
|
||||
}
|
||||
}
|
||||
|
||||
func runSimpleApiTest(tt TestCase) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
// setup server
|
||||
@@ -574,6 +631,7 @@ func runSimpleApiTest(tt TestCase) func(t *testing.T) {
|
||||
fake.FakeServiceImpl{ReturnError: tt.serviceReturnError},
|
||||
tracing.InitializeTracerForTest(),
|
||||
acimpl.ProvideAccessControlTest(),
|
||||
cloudmigration.DependencyMap{cloudmigration.PluginDataType: nil},
|
||||
)
|
||||
|
||||
server := webtest.NewServer(t, api.routeRegister)
|
||||
|
||||
@@ -269,6 +269,14 @@ type CreateSnapshotRequest struct {
|
||||
// UID of a session
|
||||
// in: path
|
||||
UID string `json:"uid"`
|
||||
|
||||
// in:body
|
||||
// required:true
|
||||
Body CreateSnapshotRequestDTO `json:"body"`
|
||||
}
|
||||
|
||||
type CreateSnapshotRequestDTO struct {
|
||||
ResourceTypes []MigrateDataType `json:"resourceTypes"`
|
||||
}
|
||||
|
||||
// swagger:response createSnapshotResponse
|
||||
@@ -395,3 +403,20 @@ type CancelSnapshotParams struct {
|
||||
// in: path
|
||||
SnapshotUID string `json:"snapshotUid"`
|
||||
}
|
||||
|
||||
// swagger:response resourceDependenciesResponse
|
||||
type ResourceDependenciesResponse struct {
|
||||
// in: body
|
||||
Body ResourceDependenciesResponseDTO
|
||||
}
|
||||
|
||||
// swagger:model ResourceDependenciesResponseDTO
|
||||
type ResourceDependenciesResponseDTO struct {
|
||||
ResourceDependencies []ResourceDependencyDTO `json:"resourceDependencies"`
|
||||
}
|
||||
|
||||
// swagger:model ResourceDependencyDTO
|
||||
type ResourceDependencyDTO struct {
|
||||
ResourceType MigrateDataType `json:"resourceType"`
|
||||
Dependencies []MigrateDataType `json:"dependencies"`
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type Service interface {
|
||||
DeleteSession(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, migUID string) (*CloudMigrationSession, error)
|
||||
GetSessionList(ctx context.Context, orgID int64) (*CloudMigrationSessionListResponse, error)
|
||||
|
||||
CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, sessionUid string) (*CloudMigrationSnapshot, error)
|
||||
CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, cmd CreateSnapshotCommand) (*CloudMigrationSnapshot, error)
|
||||
GetSnapshot(ctx context.Context, query GetSnapshotsQuery) (*CloudMigrationSnapshot, error)
|
||||
GetSnapshotList(ctx context.Context, query ListSnapshotsQuery) ([]CloudMigrationSnapshot, error)
|
||||
UploadSnapshot(ctx context.Context, orgID int64, signedInUser *user.SignedInUser, sessionUid string, snapshotUid string) error
|
||||
|
||||
@@ -13,6 +13,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
@@ -40,10 +45,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Service Define the cloudmigration.Service Implementation.
|
||||
@@ -142,7 +143,7 @@ func ProvideService(
|
||||
libraryElementsService: libraryElementsService,
|
||||
ngAlert: ngAlert,
|
||||
}
|
||||
s.api = api.RegisterApi(routeRegister, s, tracer, accessControl)
|
||||
s.api = api.RegisterApi(routeRegister, s, tracer, accessControl, cloudmigration.ResourceDependency)
|
||||
|
||||
httpClientS3, err := httpClientProvider.New()
|
||||
if err != nil {
|
||||
@@ -471,31 +472,32 @@ func (s *Service) DeleteSession(ctx context.Context, orgID int64, signedInUser *
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedInUser, cmd cloudmigration.CreateSnapshotCommand) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.CreateSnapshot", trace.WithAttributes(
|
||||
attribute.String("sessionUid", sessionUid),
|
||||
attribute.String("sessionUid", cmd.SessionUID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
if s.cfg.CloudMigration.SnapshotFolder == "" {
|
||||
return nil, fmt.Errorf("snapshot folder is not set")
|
||||
}
|
||||
|
||||
// fetch session for the gms auth token
|
||||
session, err := s.store.GetMigrationSessionByUID(ctx, signedInUser.GetOrgID(), sessionUid)
|
||||
session, err := s.store.GetMigrationSessionByUID(ctx, signedInUser.GetOrgID(), cmd.SessionUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching migration session for uid %s: %w", sessionUid, err)
|
||||
return nil, fmt.Errorf("fetching migration session for uid %s: %w", cmd.SessionUID, err)
|
||||
}
|
||||
|
||||
// query gms to establish new snapshot s.cfg.CloudMigration.StartSnapshotTimeout
|
||||
initResp, err := s.gmsClient.StartSnapshot(ctx, *session)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing snapshot with GMS for session %s: %w", sessionUid, err)
|
||||
return nil, fmt.Errorf("initializing snapshot with GMS for session %s: %w", cmd.SessionUID, err)
|
||||
}
|
||||
|
||||
if s.cfg.CloudMigration.SnapshotFolder == "" {
|
||||
return nil, fmt.Errorf("snapshot folder is not set")
|
||||
}
|
||||
// save snapshot to the db
|
||||
snapshot := cloudmigration.CloudMigrationSnapshot{
|
||||
UID: util.GenerateShortUID(),
|
||||
SessionUID: sessionUid,
|
||||
SessionUID: cmd.SessionUID,
|
||||
Status: cloudmigration.SnapshotStatusCreating,
|
||||
EncryptionKey: initResp.EncryptionKey,
|
||||
GMSSnapshotUID: initResp.SnapshotID,
|
||||
@@ -511,7 +513,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI
|
||||
// Update status to "creating" to ensure the frontend polls from now on
|
||||
if err := s.updateSnapshotWithRetries(ctx, cloudmigration.UpdateSnapshotCmd{
|
||||
UID: uid,
|
||||
SessionID: sessionUid,
|
||||
SessionID: cmd.SessionUID,
|
||||
Status: cloudmigration.SnapshotStatusCreating,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -536,7 +538,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI
|
||||
s.report(asyncCtx, session, gmsclient.EventStartBuildingSnapshot, 0, nil, signedInUser.UserUID)
|
||||
|
||||
start := time.Now()
|
||||
err := s.buildSnapshot(asyncCtx, signedInUser, initResp.MaxItemsPerPartition, initResp.Metadata, snapshot)
|
||||
err := s.buildSnapshot(asyncCtx, signedInUser, initResp.MaxItemsPerPartition, initResp.Metadata, snapshot, cmd.ResourceTypes)
|
||||
if err != nil {
|
||||
asyncSpan.SetStatus(codes.Error, "error building snapshot")
|
||||
asyncSpan.RecordError(err)
|
||||
@@ -545,7 +547,7 @@ func (s *Service) CreateSnapshot(ctx context.Context, signedInUser *user.SignedI
|
||||
// Update status to error with retries
|
||||
if err := s.updateSnapshotWithRetries(asyncCtx, cloudmigration.UpdateSnapshotCmd{
|
||||
UID: snapshot.UID,
|
||||
SessionID: sessionUid,
|
||||
SessionID: cmd.SessionUID,
|
||||
Status: cloudmigration.SnapshotStatusError,
|
||||
}); err != nil {
|
||||
s.log.Error("critical failure during snapshot creation - please report any error logs")
|
||||
|
||||
@@ -45,7 +45,7 @@ func (s *NoopServiceImpl) DeleteSession(ctx context.Context, orgID int64, signed
|
||||
return nil, cloudmigration.ErrFeatureDisabledError
|
||||
}
|
||||
|
||||
func (s *NoopServiceImpl) CreateSnapshot(ctx context.Context, user *user.SignedInUser, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
func (s *NoopServiceImpl) CreateSnapshot(ctx context.Context, user *user.SignedInUser, cmd cloudmigration.CreateSnapshotCommand) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
return nil, cloudmigration.ErrFeatureDisabledError
|
||||
}
|
||||
|
||||
|
||||
@@ -83,13 +83,13 @@ func (m FakeServiceImpl) GetSessionList(_ context.Context, _ int64) (*cloudmigra
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m FakeServiceImpl) CreateSnapshot(ctx context.Context, user *user.SignedInUser, sessionUid string) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
func (m FakeServiceImpl) CreateSnapshot(ctx context.Context, user *user.SignedInUser, cmd cloudmigration.CreateSnapshotCommand) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
if m.ReturnError {
|
||||
return nil, fmt.Errorf("mock error")
|
||||
}
|
||||
return &cloudmigration.CloudMigrationSnapshot{
|
||||
UID: "fake_uid",
|
||||
SessionUID: sessionUid,
|
||||
SessionUID: cmd.SessionUID,
|
||||
Status: cloudmigration.SnapshotStatusCreating,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ var currentMigrationTypes = []cloudmigration.MigrateDataType{
|
||||
cloudmigration.PluginDataType,
|
||||
}
|
||||
|
||||
func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser) (*cloudmigration.MigrateDataRequest, error) {
|
||||
//nolint:gocyclo
|
||||
func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.SignedInUser, resourceTypes cloudmigration.ResourceTypes) (*cloudmigration.MigrateDataRequest, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.getMigrationDataJSON")
|
||||
defer span.End()
|
||||
|
||||
@@ -56,199 +57,223 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S
|
||||
folderHierarchy := make(map[cloudmigration.MigrateDataType]map[string]string, 0)
|
||||
|
||||
// Plugins
|
||||
plugins, err := s.getPlugins(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get plugins", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.PluginDataType) {
|
||||
plugins, err := s.getPlugins(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get plugins", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.PluginDataType,
|
||||
RefID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Data: plugin.SettingCmd,
|
||||
})
|
||||
for _, plugin := range plugins {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.PluginDataType,
|
||||
RefID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Data: plugin.SettingCmd,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Data sources
|
||||
dataSources, err := s.getDataSourceCommands(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get datasources", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.DatasourceDataType) {
|
||||
dataSources, err := s.getDataSourceCommands(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get datasources", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ds := range dataSources {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: ds.UID,
|
||||
Name: ds.Name,
|
||||
Data: ds,
|
||||
})
|
||||
for _, ds := range dataSources {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: ds.UID,
|
||||
Name: ds.Name,
|
||||
Data: ds,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboards & Folders: linked via the schema, so we need to get both
|
||||
dashs, folders, err := s.getDashboardAndFolderCommands(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get dashboards and folders", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.DashboardDataType) || resourceTypes.Has(cloudmigration.FolderDataType) {
|
||||
dashs, folders, err := s.getDashboardAndFolderCommands(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get dashboards and folders", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folderHierarchy[cloudmigration.DashboardDataType] = make(map[string]string, 0)
|
||||
if resourceTypes.Has(cloudmigration.DashboardDataType) {
|
||||
folderHierarchy[cloudmigration.DashboardDataType] = make(map[string]string, 0)
|
||||
|
||||
for _, dashboard := range dashs {
|
||||
dashboard.Data.Del("id")
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.DashboardDataType,
|
||||
RefID: dashboard.UID,
|
||||
Name: dashboard.Title,
|
||||
Data: dashboards.SaveDashboardCommand{
|
||||
Dashboard: dashboard.Data,
|
||||
Overwrite: true, // currently only intended to be a push, not a sync; revisit during the preview
|
||||
Message: fmt.Sprintf("Created via the Grafana Cloud Migration Assistant by on-prem user \"%s\"", signedInUser.Login),
|
||||
IsFolder: false,
|
||||
FolderUID: dashboard.FolderUID,
|
||||
},
|
||||
})
|
||||
for _, dashboard := range dashs {
|
||||
dashboard.Data.Del("id")
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.DashboardDataType,
|
||||
RefID: dashboard.UID,
|
||||
Name: dashboard.Title,
|
||||
Data: dashboards.SaveDashboardCommand{
|
||||
Dashboard: dashboard.Data,
|
||||
Overwrite: true, // currently only intended to be a push, not a sync; revisit during the preview
|
||||
Message: fmt.Sprintf("Created via the Grafana Cloud Migration Assistant by on-prem user \"%s\"", signedInUser.Login),
|
||||
IsFolder: false,
|
||||
FolderUID: dashboard.FolderUID,
|
||||
},
|
||||
})
|
||||
|
||||
folderHierarchy[cloudmigration.DashboardDataType][dashboard.UID] = dashboard.FolderUID
|
||||
}
|
||||
folderHierarchy[cloudmigration.DashboardDataType][dashboard.UID] = dashboard.FolderUID
|
||||
}
|
||||
}
|
||||
|
||||
folderHierarchy[cloudmigration.FolderDataType] = make(map[string]string, 0)
|
||||
if resourceTypes.Has(cloudmigration.FolderDataType) {
|
||||
folderHierarchy[cloudmigration.FolderDataType] = make(map[string]string, 0)
|
||||
|
||||
folders = sortFolders(folders)
|
||||
for _, f := range folders {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.FolderDataType,
|
||||
RefID: f.UID,
|
||||
Name: f.Title,
|
||||
Data: f,
|
||||
})
|
||||
folders = sortFolders(folders)
|
||||
for _, f := range folders {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.FolderDataType,
|
||||
RefID: f.UID,
|
||||
Name: f.Title,
|
||||
Data: f,
|
||||
})
|
||||
|
||||
folderHierarchy[cloudmigration.FolderDataType][f.UID] = f.ParentUID
|
||||
folderHierarchy[cloudmigration.FolderDataType][f.UID] = f.ParentUID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Library Elements
|
||||
libraryElements, err := s.getLibraryElementsCommands(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get library elements", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.LibraryElementDataType) {
|
||||
libraryElements, err := s.getLibraryElementsCommands(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get library elements", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folderHierarchy[cloudmigration.LibraryElementDataType] = make(map[string]string, 0)
|
||||
folderHierarchy[cloudmigration.LibraryElementDataType] = make(map[string]string, 0)
|
||||
|
||||
for _, libraryElement := range libraryElements {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.LibraryElementDataType,
|
||||
RefID: libraryElement.UID,
|
||||
Name: libraryElement.Name,
|
||||
Data: libraryElement,
|
||||
})
|
||||
for _, libraryElement := range libraryElements {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.LibraryElementDataType,
|
||||
RefID: libraryElement.UID,
|
||||
Name: libraryElement.Name,
|
||||
Data: libraryElement,
|
||||
})
|
||||
|
||||
if libraryElement.FolderUID != nil {
|
||||
folderHierarchy[cloudmigration.LibraryElementDataType][libraryElement.UID] = *libraryElement.FolderUID
|
||||
if libraryElement.FolderUID != nil {
|
||||
folderHierarchy[cloudmigration.LibraryElementDataType][libraryElement.UID] = *libraryElement.FolderUID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alerts: Mute Timings
|
||||
muteTimings, err := s.getAlertMuteTimings(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert mute timings", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.MuteTimingType) {
|
||||
muteTimings, err := s.getAlertMuteTimings(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert mute timings", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, muteTiming := range muteTimings {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.MuteTimingType,
|
||||
RefID: muteTiming.UID,
|
||||
Name: muteTiming.Name,
|
||||
Data: muteTiming,
|
||||
})
|
||||
for _, muteTiming := range muteTimings {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.MuteTimingType,
|
||||
RefID: muteTiming.UID,
|
||||
Name: muteTiming.Name,
|
||||
Data: muteTiming,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Alerts: Notification Templates
|
||||
notificationTemplates, err := s.getNotificationTemplates(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert notification templates", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.NotificationTemplateType) {
|
||||
notificationTemplates, err := s.getNotificationTemplates(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert notification templates", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, notificationTemplate := range notificationTemplates {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.NotificationTemplateType,
|
||||
RefID: notificationTemplate.UID,
|
||||
Name: notificationTemplate.Name,
|
||||
Data: notificationTemplate,
|
||||
})
|
||||
for _, notificationTemplate := range notificationTemplates {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.NotificationTemplateType,
|
||||
RefID: notificationTemplate.UID,
|
||||
Name: notificationTemplate.Name,
|
||||
Data: notificationTemplate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Alerts: Contact Points
|
||||
contactPoints, err := s.getContactPoints(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert contact points", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.ContactPointType) {
|
||||
contactPoints, err := s.getContactPoints(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert contact points", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, contactPoint := range contactPoints {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.ContactPointType,
|
||||
RefID: contactPoint.UID,
|
||||
Name: contactPoint.Name,
|
||||
Data: contactPoint,
|
||||
})
|
||||
for _, contactPoint := range contactPoints {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.ContactPointType,
|
||||
RefID: contactPoint.UID,
|
||||
Name: contactPoint.Name,
|
||||
Data: contactPoint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Alerts: Notification Policies
|
||||
notificationPolicies, err := s.getNotificationPolicies(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert notification policies", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.NotificationPolicyType) {
|
||||
notificationPolicies, err := s.getNotificationPolicies(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert notification policies", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(notificationPolicies.Name) > 0 {
|
||||
// Notification Policy can only be managed by updating its entire tree, so we send the whole thing as one item.
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.NotificationPolicyType,
|
||||
RefID: notificationPolicies.Name, // no UID available
|
||||
Name: notificationPolicies.Name,
|
||||
Data: notificationPolicies.Routes,
|
||||
})
|
||||
if len(notificationPolicies.Name) > 0 {
|
||||
// Notification Policy can only be managed by updating its entire tree, so we send the whole thing as one item.
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.NotificationPolicyType,
|
||||
RefID: notificationPolicies.Name, // no UID available
|
||||
Name: notificationPolicies.Name,
|
||||
Data: notificationPolicies.Routes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Alerts: Alert Rule Groups
|
||||
alertRuleGroups, err := s.getAlertRuleGroups(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert rule groups", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.AlertRuleGroupType) {
|
||||
alertRuleGroups, err := s.getAlertRuleGroups(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert rule groups", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, alertRuleGroup := range alertRuleGroups {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.AlertRuleGroupType,
|
||||
RefID: alertRuleGroup.Title, // no UID available
|
||||
Name: alertRuleGroup.Title,
|
||||
Data: alertRuleGroup,
|
||||
})
|
||||
for _, alertRuleGroup := range alertRuleGroups {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.AlertRuleGroupType,
|
||||
RefID: alertRuleGroup.Title, // no UID available
|
||||
Name: alertRuleGroup.Title,
|
||||
Data: alertRuleGroup,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Alerts: Alert Rules
|
||||
alertRules, err := s.getAlertRules(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert rules", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
if resourceTypes.Has(cloudmigration.AlertRuleType) {
|
||||
alertRules, err := s.getAlertRules(ctx, signedInUser)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get alert rules", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folderHierarchy[cloudmigration.AlertRuleType] = make(map[string]string, 0)
|
||||
folderHierarchy[cloudmigration.AlertRuleType] = make(map[string]string, 0)
|
||||
|
||||
for _, alertRule := range alertRules {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.AlertRuleType,
|
||||
RefID: alertRule.UID,
|
||||
Name: alertRule.Title,
|
||||
Data: alertRule,
|
||||
})
|
||||
for _, alertRule := range alertRules {
|
||||
migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{
|
||||
Type: cloudmigration.AlertRuleType,
|
||||
RefID: alertRule.UID,
|
||||
Name: alertRule.Title,
|
||||
Data: alertRule,
|
||||
})
|
||||
|
||||
folderHierarchy[cloudmigration.AlertRuleType][alertRule.UID] = alertRule.FolderUID
|
||||
folderHierarchy[cloudmigration.AlertRuleType][alertRule.UID] = alertRule.FolderUID
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain the names of parent elements for data types that have folders.
|
||||
@@ -508,7 +533,14 @@ func (s *Service) getPlugins(ctx context.Context, signedInUser *user.SignedInUse
|
||||
}
|
||||
|
||||
// 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,
|
||||
resourceTypes cloudmigration.ResourceTypes,
|
||||
) error {
|
||||
ctx, span := s.tracer.Start(ctx, "CloudMigrationService.buildSnapshot")
|
||||
defer span.End()
|
||||
|
||||
@@ -542,7 +574,7 @@ func (s *Service) buildSnapshot(ctx context.Context, signedInUser *user.SignedIn
|
||||
|
||||
s.log.Debug(fmt.Sprintf("buildSnapshot: created snapshot writing in %d ms", time.Since(start).Milliseconds()))
|
||||
|
||||
migrationData, err := s.getMigrationDataJSON(ctx, signedInUser)
|
||||
migrationData, err := s.getMigrationDataJSON(ctx, signedInUser, resourceTypes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching migration data: %w", err)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ var (
|
||||
ErrMigrationNotDeleted = errutil.Internal("cloudmigrations.sessionNotDeleted").Errorf("Session not deleted")
|
||||
ErrTokenNotFound = errutil.NotFound("cloudmigrations.tokenNotFound").Errorf("Token not found")
|
||||
ErrSnapshotNotFound = errutil.NotFound("cloudmigrations.snapshotNotFound").Errorf("Snapshot not found")
|
||||
ErrEmptyResourceTypes = errutil.BadRequest("cloudmigrations.emptyResourceTypes").Errorf("Resource types cannot be empty")
|
||||
)
|
||||
|
||||
// CloudMigration domain structs
|
||||
@@ -192,6 +193,18 @@ type SnapshotResultQueryParams struct {
|
||||
ErrorsOnly bool
|
||||
}
|
||||
|
||||
type ResourceTypes map[MigrateDataType]struct{}
|
||||
|
||||
func (r ResourceTypes) Has(t MigrateDataType) bool {
|
||||
_, ok := r[t]
|
||||
return ok
|
||||
}
|
||||
|
||||
type CreateSnapshotCommand struct {
|
||||
SessionUID string
|
||||
ResourceTypes ResourceTypes
|
||||
}
|
||||
|
||||
type GetSnapshotsQuery struct {
|
||||
SnapshotUID string
|
||||
OrgID int64
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package cloudmigration
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDuplicateResourceType = errutil.BadRequest("cloudmigrations.duplicateResourceType")
|
||||
ErrUnknownResourceType = errutil.BadRequest("cloudmigrations.unknownResourceType")
|
||||
ErrMissingDependency = errutil.BadRequest("cloudmigrations.missingDependency")
|
||||
)
|
||||
|
||||
// DependencyMap is a map of resource types to their direct dependencies.
|
||||
type DependencyMap map[MigrateDataType][]MigrateDataType
|
||||
|
||||
// ResourceDependency is a map of resource types to their direct dependencies.
|
||||
// This is used to determine which resources can be filtered out from the snapshot without breaking the dependency chain.
|
||||
var ResourceDependency = DependencyMap{
|
||||
PluginDataType: nil,
|
||||
FolderDataType: nil,
|
||||
DatasourceDataType: {PluginDataType},
|
||||
LibraryElementDataType: {FolderDataType},
|
||||
DashboardDataType: {FolderDataType, DatasourceDataType, LibraryElementDataType},
|
||||
MuteTimingType: nil,
|
||||
NotificationTemplateType: nil,
|
||||
ContactPointType: {NotificationTemplateType},
|
||||
NotificationPolicyType: {ContactPointType},
|
||||
AlertRuleType: {DatasourceDataType, FolderDataType, DashboardDataType, MuteTimingType, ContactPointType, NotificationPolicyType},
|
||||
AlertRuleGroupType: {AlertRuleType},
|
||||
}
|
||||
|
||||
// Parse a raw slice of resource types and returns a set of them if it has all correct dependencies.
|
||||
func (depMap DependencyMap) Parse(rawInput []MigrateDataType) (ResourceTypes, error) {
|
||||
// Clean up any possible duplicates.
|
||||
input := make(ResourceTypes, len(rawInput))
|
||||
for _, resourceType := range rawInput {
|
||||
if _, exists := input[resourceType]; exists {
|
||||
return nil, ErrDuplicateResourceType.Errorf("duplicate resource type found: %v", resourceType)
|
||||
}
|
||||
input[resourceType] = struct{}{}
|
||||
}
|
||||
|
||||
// Validate that all dependencies are present.
|
||||
for resourceType := range input {
|
||||
if err := depMap.validateDependencies(resourceType, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// validateDependencies recursively checks if all dependencies for a resource type are present in the input set.
|
||||
func (depMap DependencyMap) validateDependencies(resourceType MigrateDataType, input ResourceTypes) error {
|
||||
// Get the direct dependencies for this resource type.
|
||||
dependencies, ok := depMap[resourceType]
|
||||
if !ok {
|
||||
return ErrUnknownResourceType.Errorf("unknown resource type: %v", resourceType)
|
||||
}
|
||||
|
||||
// Make sure all direct dependencies are in the input.
|
||||
for _, dep := range dependencies {
|
||||
if _, exists := input[dep]; !exists {
|
||||
return ErrMissingDependency.Errorf("missing dependency: %v for resource type %v", dep, resourceType)
|
||||
}
|
||||
|
||||
// Recursively validate dependencies of dependencies
|
||||
if err := depMap.validateDependencies(dep, input); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package cloudmigration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResourceDependencyParse(t *testing.T) {
|
||||
t.Run("empty input returns an empty set", func(t *testing.T) {
|
||||
result, err := ResourceDependency.Parse(nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("input with duplicates returns an error", func(t *testing.T) {
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{FolderDataType, FolderDataType})
|
||||
require.ErrorIs(t, err, ErrDuplicateResourceType)
|
||||
})
|
||||
|
||||
t.Run("unknown resource type returns an error", func(t *testing.T) {
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{"does not exist"})
|
||||
require.ErrorIs(t, err, ErrUnknownResourceType)
|
||||
})
|
||||
|
||||
t.Run("PluginDataType has no dependencies", func(t *testing.T) {
|
||||
result, err := ResourceDependency.Parse([]MigrateDataType{PluginDataType})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
require.Contains(t, result, PluginDataType)
|
||||
})
|
||||
|
||||
t.Run("FolderDataType has no dependencies", func(t *testing.T) {
|
||||
result, err := ResourceDependency.Parse([]MigrateDataType{FolderDataType})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
require.Contains(t, result, FolderDataType)
|
||||
})
|
||||
|
||||
t.Run("DatasourceDataType requires PluginDataType", func(t *testing.T) {
|
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) {
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{DatasourceDataType})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
require.Contains(t, err.Error(), string(PluginDataType))
|
||||
})
|
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) {
|
||||
input := []MigrateDataType{DatasourceDataType, PluginDataType}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("LibraryElementDataType requires FolderDataType", func(t *testing.T) {
|
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) {
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{LibraryElementDataType})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
require.Contains(t, err.Error(), string(FolderDataType))
|
||||
})
|
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) {
|
||||
input := []MigrateDataType{LibraryElementDataType, FolderDataType}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("DashboardDataType requires multiple dependencies", func(t *testing.T) {
|
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) {
|
||||
// Missing: FolderDataType, DatasourceDataType, LibraryElementDataType, PluginDataType
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{DashboardDataType})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
|
||||
// Missing: DatasourceDataType, PluginDataType
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{
|
||||
DashboardDataType,
|
||||
LibraryElementDataType,
|
||||
FolderDataType,
|
||||
})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
})
|
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) {
|
||||
input := []MigrateDataType{
|
||||
DashboardDataType,
|
||||
FolderDataType,
|
||||
DatasourceDataType,
|
||||
LibraryElementDataType,
|
||||
PluginDataType,
|
||||
}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, len(input))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("MuteTimingType has no dependencies", func(t *testing.T) {
|
||||
result, err := ResourceDependency.Parse([]MigrateDataType{MuteTimingType})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
require.Contains(t, result, MuteTimingType)
|
||||
})
|
||||
|
||||
t.Run("NotificationTemplateType has no dependencies", func(t *testing.T) {
|
||||
result, err := ResourceDependency.Parse([]MigrateDataType{NotificationTemplateType})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
require.Contains(t, result, NotificationTemplateType)
|
||||
})
|
||||
|
||||
t.Run("ContactPointType requires NotificationTemplateType", func(t *testing.T) {
|
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) {
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{ContactPointType})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
require.Contains(t, err.Error(), string(NotificationTemplateType))
|
||||
})
|
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) {
|
||||
input := []MigrateDataType{ContactPointType, NotificationTemplateType}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("NotificationPolicyType requires multiple dependencies", func(t *testing.T) {
|
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) {
|
||||
// Missing: ContactPointType, NotificationTemplateType
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{NotificationPolicyType})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
|
||||
// Missing: NotificationTemplateType
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{
|
||||
NotificationPolicyType,
|
||||
ContactPointType,
|
||||
})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
})
|
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) {
|
||||
input := []MigrateDataType{
|
||||
NotificationPolicyType,
|
||||
ContactPointType,
|
||||
NotificationTemplateType,
|
||||
}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 3)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AlertRuleType requires multiple dependencies", func(t *testing.T) {
|
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) {
|
||||
// Missing all dependencies
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{AlertRuleType})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
|
||||
// Missing some dependencies
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{
|
||||
AlertRuleType,
|
||||
DatasourceDataType,
|
||||
FolderDataType,
|
||||
})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
})
|
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) {
|
||||
input := []MigrateDataType{
|
||||
AlertRuleType,
|
||||
DatasourceDataType,
|
||||
FolderDataType,
|
||||
DashboardDataType,
|
||||
MuteTimingType,
|
||||
ContactPointType,
|
||||
NotificationPolicyType,
|
||||
PluginDataType,
|
||||
LibraryElementDataType,
|
||||
NotificationTemplateType,
|
||||
}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, len(input))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AlertRuleGroupType requires AlertRuleType and all its dependencies", func(t *testing.T) {
|
||||
t.Run("when the dependency is missing returns an error", func(t *testing.T) {
|
||||
// Missing all dependencies
|
||||
_, err := ResourceDependency.Parse([]MigrateDataType{AlertRuleGroupType})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
|
||||
// With partial dependencies
|
||||
_, err = ResourceDependency.Parse([]MigrateDataType{
|
||||
AlertRuleGroupType,
|
||||
AlertRuleType,
|
||||
FolderDataType,
|
||||
DashboardDataType,
|
||||
MuteTimingType,
|
||||
})
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
})
|
||||
|
||||
t.Run("when the dependency is present returns the correct set", func(t *testing.T) {
|
||||
input := []MigrateDataType{
|
||||
AlertRuleGroupType,
|
||||
AlertRuleType,
|
||||
DatasourceDataType,
|
||||
FolderDataType,
|
||||
DashboardDataType,
|
||||
MuteTimingType,
|
||||
ContactPointType,
|
||||
NotificationPolicyType,
|
||||
PluginDataType,
|
||||
LibraryElementDataType,
|
||||
NotificationTemplateType,
|
||||
}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, len(input))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("multiple resources with shared dependencies", func(t *testing.T) {
|
||||
t.Run("resources with no dependencies", func(t *testing.T) {
|
||||
input := []MigrateDataType{
|
||||
FolderDataType,
|
||||
PluginDataType,
|
||||
MuteTimingType,
|
||||
NotificationTemplateType,
|
||||
}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 4)
|
||||
})
|
||||
|
||||
t.Run("overlapping dependencies", func(t *testing.T) {
|
||||
// DashboardDataType -> LibraryElementDataType -> FolderDataType
|
||||
// \-> DatasourceDataType -> PluginDataType
|
||||
// ContactPointType -> NotificationTemplateType
|
||||
input := []MigrateDataType{
|
||||
DashboardDataType,
|
||||
LibraryElementDataType,
|
||||
FolderDataType,
|
||||
DatasourceDataType,
|
||||
PluginDataType,
|
||||
ContactPointType,
|
||||
NotificationTemplateType,
|
||||
}
|
||||
result, err := ResourceDependency.Parse(input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 7)
|
||||
})
|
||||
|
||||
t.Run("missing shared dependency fails", func(t *testing.T) {
|
||||
// ContactPointType -> NotificationTemplateType
|
||||
// DatasourceDataType -> PluginDataType
|
||||
input := []MigrateDataType{
|
||||
ContactPointType,
|
||||
DatasourceDataType,
|
||||
}
|
||||
_, err := ResourceDependency.Parse(input)
|
||||
require.ErrorIs(t, err, ErrMissingDependency)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -2412,6 +2412,14 @@
|
||||
"name": "uid",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/CreateSnapshotRequestDTO"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -2654,6 +2662,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cloudmigration/resources/dependencies": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"migrations"
|
||||
],
|
||||
"summary": "Get the resource dependencies graph for the current set of migratable resources.",
|
||||
"operationId": "getResourceDependencies",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/resourceDependenciesResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cloudmigration/token": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -14343,6 +14365,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateSnapshotRequestDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceTypes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER",
|
||||
"LIBRARY_ELEMENT",
|
||||
"ALERT_RULE",
|
||||
"ALERT_RULE_GROUP",
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateSnapshotResponseDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19690,6 +19736,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResourceDependenciesResponseDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resourceDependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ResourceDependencyDTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResourceDependencyDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER",
|
||||
"LIBRARY_ELEMENT",
|
||||
"ALERT_RULE",
|
||||
"ALERT_RULE_GROUP",
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
]
|
||||
}
|
||||
},
|
||||
"resourceType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER",
|
||||
"LIBRARY_ELEMENT",
|
||||
"ALERT_RULE",
|
||||
"ALERT_RULE_GROUP",
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResponseDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24563,6 +24660,12 @@
|
||||
"$ref": "#/definitions/ActiveUserStats"
|
||||
}
|
||||
},
|
||||
"resourceDependenciesResponse": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ResourceDependenciesResponseDTO"
|
||||
}
|
||||
},
|
||||
"resourcePermissionsDescription": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
|
||||
@@ -18,7 +18,11 @@ const injectedRtkApi = api.injectEndpoints({
|
||||
query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}` }),
|
||||
}),
|
||||
createSnapshot: build.mutation<CreateSnapshotApiResponse, CreateSnapshotApiArg>({
|
||||
query: (queryArg) => ({ url: `/cloudmigration/migration/${queryArg.uid}/snapshot`, method: 'POST' }),
|
||||
query: (queryArg) => ({
|
||||
url: `/cloudmigration/migration/${queryArg.uid}/snapshot`,
|
||||
method: 'POST',
|
||||
body: queryArg.createSnapshotRequestDto,
|
||||
}),
|
||||
}),
|
||||
getSnapshot: build.query<GetSnapshotApiResponse, GetSnapshotApiArg>({
|
||||
query: (queryArg) => ({
|
||||
@@ -54,6 +58,9 @@ const injectedRtkApi = api.injectEndpoints({
|
||||
},
|
||||
}),
|
||||
}),
|
||||
getResourceDependencies: build.query<GetResourceDependenciesApiResponse, GetResourceDependenciesApiArg>({
|
||||
query: () => ({ url: `/cloudmigration/resources/dependencies` }),
|
||||
}),
|
||||
getCloudMigrationToken: build.query<GetCloudMigrationTokenApiResponse, GetCloudMigrationTokenApiArg>({
|
||||
query: () => ({ url: `/cloudmigration/token` }),
|
||||
}),
|
||||
@@ -93,6 +100,7 @@ export type CreateSnapshotApiResponse = /** status 200 (empty) */ CreateSnapshot
|
||||
export type CreateSnapshotApiArg = {
|
||||
/** UID of a session */
|
||||
uid: string;
|
||||
createSnapshotRequestDto: CreateSnapshotRequestDto;
|
||||
};
|
||||
export type GetSnapshotApiResponse = /** status 200 (empty) */ GetSnapshotResponseDto;
|
||||
export type GetSnapshotApiArg = {
|
||||
@@ -136,6 +144,8 @@ export type GetShapshotListApiArg = {
|
||||
/** Sort with value latest to return results sorted in descending order. */
|
||||
sort?: string;
|
||||
};
|
||||
export type GetResourceDependenciesApiResponse = /** status 200 (empty) */ ResourceDependenciesResponseDto;
|
||||
export type GetResourceDependenciesApiArg = void;
|
||||
export type GetCloudMigrationTokenApiResponse = /** status 200 (empty) */ GetAccessTokenResponseDto;
|
||||
export type GetCloudMigrationTokenApiArg = void;
|
||||
export type CreateCloudMigrationTokenApiResponse = /** status 200 (empty) */ CreateAccessTokenResponseDto;
|
||||
@@ -179,6 +189,21 @@ export type CloudMigrationSessionRequestDto = {
|
||||
export type CreateSnapshotResponseDto = {
|
||||
uid?: string;
|
||||
};
|
||||
export type CreateSnapshotRequestDto = {
|
||||
resourceTypes?: (
|
||||
| 'DASHBOARD'
|
||||
| 'DATASOURCE'
|
||||
| 'FOLDER'
|
||||
| 'LIBRARY_ELEMENT'
|
||||
| 'ALERT_RULE'
|
||||
| 'ALERT_RULE_GROUP'
|
||||
| 'CONTACT_POINT'
|
||||
| 'NOTIFICATION_POLICY'
|
||||
| 'NOTIFICATION_TEMPLATE'
|
||||
| 'MUTE_TIMING'
|
||||
| 'PLUGIN'
|
||||
)[];
|
||||
};
|
||||
export type MigrateDataResponseItemDto = {
|
||||
errorCode?:
|
||||
| 'DATASOURCE_NAME_CONFLICT'
|
||||
@@ -258,6 +283,36 @@ export type SnapshotDto = {
|
||||
export type SnapshotListResponseDto = {
|
||||
snapshots?: SnapshotDto[];
|
||||
};
|
||||
export type ResourceDependencyDto = {
|
||||
dependencies?: (
|
||||
| 'DASHBOARD'
|
||||
| 'DATASOURCE'
|
||||
| 'FOLDER'
|
||||
| 'LIBRARY_ELEMENT'
|
||||
| 'ALERT_RULE'
|
||||
| 'ALERT_RULE_GROUP'
|
||||
| 'CONTACT_POINT'
|
||||
| 'NOTIFICATION_POLICY'
|
||||
| 'NOTIFICATION_TEMPLATE'
|
||||
| 'MUTE_TIMING'
|
||||
| 'PLUGIN'
|
||||
)[];
|
||||
resourceType?:
|
||||
| 'DASHBOARD'
|
||||
| 'DATASOURCE'
|
||||
| 'FOLDER'
|
||||
| 'LIBRARY_ELEMENT'
|
||||
| 'ALERT_RULE'
|
||||
| 'ALERT_RULE_GROUP'
|
||||
| 'CONTACT_POINT'
|
||||
| 'NOTIFICATION_POLICY'
|
||||
| 'NOTIFICATION_TEMPLATE'
|
||||
| 'MUTE_TIMING'
|
||||
| 'PLUGIN';
|
||||
};
|
||||
export type ResourceDependenciesResponseDto = {
|
||||
resourceDependencies?: ResourceDependencyDto[];
|
||||
};
|
||||
export type GetAccessTokenResponseDto = {
|
||||
createdAt?: string;
|
||||
displayName?: string;
|
||||
@@ -356,6 +411,7 @@ export const {
|
||||
useCancelSnapshotMutation,
|
||||
useUploadSnapshotMutation,
|
||||
useGetShapshotListQuery,
|
||||
useGetResourceDependenciesQuery,
|
||||
useGetCloudMigrationTokenQuery,
|
||||
useCreateCloudMigrationTokenMutation,
|
||||
useDeleteCloudMigrationTokenMutation,
|
||||
|
||||
@@ -26,7 +26,12 @@ export const cloudMigrationAPI = generatedAPI
|
||||
}),
|
||||
})
|
||||
.enhanceEndpoints({
|
||||
addTagTypes: ['cloud-migration-token', 'cloud-migration-session', 'cloud-migration-snapshot'],
|
||||
addTagTypes: [
|
||||
'cloud-migration-token',
|
||||
'cloud-migration-session',
|
||||
'cloud-migration-snapshot',
|
||||
'cloud-migration-resource-dependencies',
|
||||
],
|
||||
|
||||
endpoints: {
|
||||
// Cloud-side - create token
|
||||
@@ -68,6 +73,11 @@ export const cloudMigrationAPI = generatedAPI
|
||||
invalidatesTags: ['cloud-migration-snapshot'],
|
||||
},
|
||||
|
||||
// Resource dependencies
|
||||
getResourceDependencies: {
|
||||
providesTags: ['cloud-migration-resource-dependencies'],
|
||||
},
|
||||
|
||||
getDashboardByUid: suppressErrorsOnQuery,
|
||||
getLibraryElementByUid: suppressErrorsOnQuery,
|
||||
getLocalPluginList: suppressErrorsOnQuery,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
useUploadSnapshotMutation,
|
||||
useGetLocalPluginListQuery,
|
||||
} from '../api';
|
||||
import { maybeAPIError } from '../api/errors';
|
||||
import { AlertWithTraceID } from '../shared/AlertWithTraceID';
|
||||
|
||||
import { DisconnectModal } from './DisconnectModal';
|
||||
@@ -199,7 +200,26 @@ export const Page = () => {
|
||||
|
||||
const handleCreateSnapshot = useCallback(() => {
|
||||
if (sessionUid) {
|
||||
performCreateSnapshot({ uid: sessionUid });
|
||||
performCreateSnapshot({
|
||||
uid: sessionUid,
|
||||
createSnapshotRequestDto: {
|
||||
// TODO: For the moment, pass all resource types. Once we have a frontend for selecting resource types,
|
||||
// we should pass the selected resource types instead.
|
||||
resourceTypes: [
|
||||
'DASHBOARD',
|
||||
'DATASOURCE',
|
||||
'FOLDER',
|
||||
'LIBRARY_ELEMENT',
|
||||
'ALERT_RULE',
|
||||
'ALERT_RULE_GROUP',
|
||||
'CONTACT_POINT',
|
||||
'NOTIFICATION_POLICY',
|
||||
'NOTIFICATION_TEMPLATE',
|
||||
'MUTE_TIMING',
|
||||
'PLUGIN',
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [performCreateSnapshot, sessionUid]);
|
||||
|
||||
@@ -367,12 +387,7 @@ function getError(props: GetErrorProps): ErrorDescription | undefined {
|
||||
}
|
||||
|
||||
if (createSnapshotError) {
|
||||
return {
|
||||
severity: 'warning',
|
||||
title: t('migrate-to-cloud.onprem.create-snapshot-error-title', 'Error creating snapshot'),
|
||||
body: seeLogs,
|
||||
error: createSnapshotError,
|
||||
};
|
||||
return handleCreateSnapshotError(createSnapshotError, seeLogs);
|
||||
}
|
||||
|
||||
if (uploadSnapshotError) {
|
||||
@@ -431,3 +446,51 @@ function getError(props: GetErrorProps): ErrorDescription | undefined {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleCreateSnapshotError(createSnapshotError: unknown, seeLogs: string): ErrorDescription | undefined {
|
||||
const apiError = maybeAPIError(createSnapshotError);
|
||||
|
||||
let severity: AlertVariant = 'warning';
|
||||
let body = null;
|
||||
|
||||
switch (apiError?.messageId) {
|
||||
case 'cloudmigrations.emptyResourceTypes':
|
||||
severity = 'error';
|
||||
body = t(
|
||||
'migrate-to-cloud.onprem.create-snapshot-error-empty-resource-types',
|
||||
'You need to provide at least one resource type for snapshot creation'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'cloudmigrations.unknownResourceType':
|
||||
severity = 'error';
|
||||
body = t(
|
||||
'migrate-to-cloud.onprem.create-snapshot-error-unknown-resource-type',
|
||||
'Unknown resource type. See the Grafana server logs for more details'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'cloudmigrations.duplicateResourceType':
|
||||
severity = 'error';
|
||||
body = t(
|
||||
'migrate-to-cloud.onprem.create-snapshot-error-duplicate-resource-type',
|
||||
'Duplicate resource type. See the Grafana server logs for more details'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'cloudmigrations.missingDependency':
|
||||
severity = 'error';
|
||||
body = t(
|
||||
'migrate-to-cloud.onprem.create-snapshot-error-missing-dependency',
|
||||
'Missing dependency. See the Grafana server logs for more details'
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
severity,
|
||||
title: t('migrate-to-cloud.onprem.create-snapshot-error-title', 'Error creating snapshot'),
|
||||
body: body || seeLogs,
|
||||
error: createSnapshotError,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5336,7 +5336,11 @@
|
||||
},
|
||||
"onprem": {
|
||||
"cancel-snapshot-error-title": "Error cancelling creating snapshot",
|
||||
"create-snapshot-error-duplicate-resource-type": "Duplicate resource type. See the Grafana server logs for more details",
|
||||
"create-snapshot-error-empty-resource-types": "You need to provide at least one resource type for snapshot creation",
|
||||
"create-snapshot-error-missing-dependency": "Missing dependency. See the Grafana server logs for more details",
|
||||
"create-snapshot-error-title": "Error creating snapshot",
|
||||
"create-snapshot-error-unknown-resource-type": "Unknown resource type. See the Grafana server logs for more details",
|
||||
"disconnect-error-title": "Error disconnecting",
|
||||
"error-see-server-logs": "See the Grafana server logs for more details",
|
||||
"get-session-error-title": "Error loading migration configuration",
|
||||
|
||||
@@ -1803,6 +1803,16 @@
|
||||
},
|
||||
"description": "(empty)"
|
||||
},
|
||||
"resourceDependenciesResponse": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResourceDependenciesResponseDTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "(empty)"
|
||||
},
|
||||
"resourcePermissionsDescription": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -4417,6 +4427,30 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CreateSnapshotRequestDTO": {
|
||||
"properties": {
|
||||
"resourceTypes": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER",
|
||||
"LIBRARY_ELEMENT",
|
||||
"ALERT_RULE",
|
||||
"ALERT_RULE_GROUP",
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"CreateSnapshotResponseDTO": {
|
||||
"properties": {
|
||||
"uid": {
|
||||
@@ -9764,6 +9798,57 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ResourceDependenciesResponseDTO": {
|
||||
"properties": {
|
||||
"resourceDependencies": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ResourceDependencyDTO"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ResourceDependencyDTO": {
|
||||
"properties": {
|
||||
"dependencies": {
|
||||
"items": {
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER",
|
||||
"LIBRARY_ELEMENT",
|
||||
"ALERT_RULE",
|
||||
"ALERT_RULE_GROUP",
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"resourceType": {
|
||||
"enum": [
|
||||
"DASHBOARD",
|
||||
"DATASOURCE",
|
||||
"FOLDER",
|
||||
"LIBRARY_ELEMENT",
|
||||
"ALERT_RULE",
|
||||
"ALERT_RULE_GROUP",
|
||||
"CONTACT_POINT",
|
||||
"NOTIFICATION_POLICY",
|
||||
"NOTIFICATION_TEMPLATE",
|
||||
"MUTE_TIMING",
|
||||
"PLUGIN"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ResponseDetails": {
|
||||
"properties": {
|
||||
"msg": {
|
||||
@@ -15978,6 +16063,17 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateSnapshotRequestDTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true,
|
||||
"x-originalParamName": "body"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/createSnapshotResponse"
|
||||
@@ -16252,6 +16348,20 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/cloudmigration/resources/dependencies": {
|
||||
"get": {
|
||||
"operationId": "getResourceDependencies",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/resourceDependenciesResponse"
|
||||
}
|
||||
},
|
||||
"summary": "Get the resource dependencies graph for the current set of migratable resources.",
|
||||
"tags": [
|
||||
"migrations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/cloudmigration/token": {
|
||||
"get": {
|
||||
"operationId": "getCloudMigrationToken",
|
||||
|
||||
@@ -30,6 +30,8 @@ const config: ConfigFile = {
|
||||
|
||||
'getDashboardByUid',
|
||||
'getLibraryElementByUid',
|
||||
|
||||
'getResourceDependencies',
|
||||
],
|
||||
},
|
||||
'../public/app/features/preferences/api/user/endpoints.gen.ts': {
|
||||
|
||||
Reference in New Issue
Block a user