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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user