CloudMigrations: Refactor API for async work (#89084)
* rename some stuff * more renaming * clean up api * rename more functions * rename cms -> gms * update comment * update swagger gen * update endpoints * overzealous * final touches * dont modify existing migrations * break structs into domain and dtos * add some conversion funcs * fix build * update frontend * try to make swagger happy
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package gmsclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
ValidateKey(context.Context, cloudmigration.CloudMigrationSession) error
|
||||
MigrateData(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.MigrateDataRequest) (*cloudmigration.MigrateDataResponse, error)
|
||||
}
|
||||
|
||||
const logPrefix = "cloudmigration.gmsclient"
|
||||
@@ -0,0 +1,47 @@
|
||||
// TODO: Move these to a shared library in common with GMS
|
||||
package gmsclient
|
||||
|
||||
type MigrateDataType string
|
||||
|
||||
const (
|
||||
DashboardDataType MigrateDataType = "DASHBOARD"
|
||||
DatasourceDataType MigrateDataType = "DATASOURCE"
|
||||
FolderDataType MigrateDataType = "FOLDER"
|
||||
)
|
||||
|
||||
type MigrateDataRequestDTO struct {
|
||||
Items []MigrateDataRequestItemDTO `json:"items"`
|
||||
}
|
||||
|
||||
type MigrateDataRequestItemDTO struct {
|
||||
Type MigrateDataType `json:"type"`
|
||||
RefID string `json:"refId"`
|
||||
Name string `json:"name"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type ItemStatus string
|
||||
|
||||
const (
|
||||
ItemStatusOK ItemStatus = "OK"
|
||||
ItemStatusError ItemStatus = "ERROR"
|
||||
)
|
||||
|
||||
type MigrateDataResponseDTO struct {
|
||||
RunUID string `json:"uid"`
|
||||
Items []MigrateDataResponseItemDTO `json:"items"`
|
||||
}
|
||||
|
||||
type MigrateDataResponseListDTO struct {
|
||||
RunUID string `json:"uid"`
|
||||
}
|
||||
|
||||
type MigrateDataResponseItemDTO struct {
|
||||
// required:true
|
||||
Type MigrateDataType `json:"type"`
|
||||
// required:true
|
||||
RefID string `json:"refId"`
|
||||
// required:true
|
||||
Status ItemStatus `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package gmsclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
)
|
||||
|
||||
// NewGMSClient returns an implementation of Client that queries GrafanaMigrationService
|
||||
func NewGMSClient(domain string) Client {
|
||||
return &gmsClientImpl{
|
||||
domain: domain,
|
||||
log: log.New(logPrefix),
|
||||
}
|
||||
}
|
||||
|
||||
type gmsClientImpl struct {
|
||||
domain string
|
||||
log *log.ConcreteLogger
|
||||
}
|
||||
|
||||
func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.CloudMigrationSession) error {
|
||||
logger := c.log.FromContext(ctx)
|
||||
|
||||
// TODO update service url to gms
|
||||
path := fmt.Sprintf("https://cms-%s.%s/cloud-migrations/api/v1/validate-key", cm.ClusterSlug, c.domain)
|
||||
|
||||
// validation is an empty POST to GMS with the authorization header included
|
||||
req, err := http.NewRequest("POST", path, bytes.NewReader(nil))
|
||||
if err != nil {
|
||||
logger.Error("error creating http request for token validation", "err", err.Error())
|
||||
return fmt.Errorf("http request error: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", cm.StackID, cm.AuthToken))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Error("error sending http request for token validation", "err", err.Error())
|
||||
return fmt.Errorf("http request error: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Error("closing request body", "err", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
var errResp map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
|
||||
logger.Error("decoding error response", "err", err.Error())
|
||||
} else {
|
||||
return fmt.Errorf("token validation failure: %v", errResp)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *gmsClientImpl) MigrateData(ctx context.Context, cm cloudmigration.CloudMigrationSession, request cloudmigration.MigrateDataRequest) (*cloudmigration.MigrateDataResponse, error) {
|
||||
logger := c.log.FromContext(ctx)
|
||||
|
||||
// TODO update service url to gms
|
||||
path := fmt.Sprintf("https://cms-%s.%s/cloud-migrations/api/v1/migrate-data", cm.ClusterSlug, c.domain)
|
||||
|
||||
reqDTO := convertRequestToDTO(request)
|
||||
body, err := json.Marshal(reqDTO)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling request: %w", err)
|
||||
}
|
||||
|
||||
// Send the request to GMS with the associated auth token
|
||||
req, err := http.NewRequest(http.MethodPost, path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
c.log.Error("error creating http request for cloud migration run", "err", err.Error())
|
||||
return nil, fmt.Errorf("http request error: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", cm.StackID, cm.AuthToken))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.log.Error("error sending http request for cloud migration run", "err", err.Error())
|
||||
return nil, fmt.Errorf("http request error: %w", err)
|
||||
} else if resp.StatusCode >= 400 {
|
||||
c.log.Error("received error response for cloud migration run", "statusCode", resp.StatusCode)
|
||||
return nil, fmt.Errorf("http request error: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Error("closing request body: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var respDTO MigrateDataResponseDTO
|
||||
if err := json.NewDecoder(resp.Body).Decode(&respDTO); err != nil {
|
||||
logger.Error("unmarshalling response body: %w", err)
|
||||
return nil, fmt.Errorf("unmarshalling migration run response: %w", err)
|
||||
}
|
||||
|
||||
result := convertResponseFromDTO(respDTO)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func convertRequestToDTO(request cloudmigration.MigrateDataRequest) MigrateDataRequestDTO {
|
||||
items := make([]MigrateDataRequestItemDTO, len(request.Items))
|
||||
for i := 0; i < len(request.Items); i++ {
|
||||
item := request.Items[i]
|
||||
items[i] = MigrateDataRequestItemDTO{
|
||||
Type: MigrateDataType(item.Type),
|
||||
RefID: item.RefID,
|
||||
Name: item.Name,
|
||||
Data: item.Data,
|
||||
}
|
||||
}
|
||||
r := MigrateDataRequestDTO{
|
||||
Items: items,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func convertResponseFromDTO(result MigrateDataResponseDTO) cloudmigration.MigrateDataResponse {
|
||||
items := make([]cloudmigration.MigrateDataResponseItem, len(result.Items))
|
||||
for i := 0; i < len(result.Items); i++ {
|
||||
item := result.Items[i]
|
||||
items[i] = cloudmigration.MigrateDataResponseItem{
|
||||
Type: cloudmigration.MigrateDataType(item.Type),
|
||||
RefID: item.RefID,
|
||||
Status: cloudmigration.ItemStatus(item.Status),
|
||||
Error: item.Error,
|
||||
}
|
||||
}
|
||||
return cloudmigration.MigrateDataResponse{
|
||||
RunUID: result.RunUID,
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package gmsclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
)
|
||||
|
||||
// NewInMemoryClient returns an implementation of Client that returns canned responses
|
||||
func NewInMemoryClient() Client {
|
||||
return &memoryClientImpl{}
|
||||
}
|
||||
|
||||
type memoryClientImpl struct{}
|
||||
|
||||
func (c *memoryClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.CloudMigrationSession) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memoryClientImpl) MigrateData(
|
||||
ctx context.Context,
|
||||
cm cloudmigration.CloudMigrationSession,
|
||||
request cloudmigration.MigrateDataRequest,
|
||||
) (*cloudmigration.MigrateDataResponse, error) {
|
||||
result := cloudmigration.MigrateDataResponse{
|
||||
Items: make([]cloudmigration.MigrateDataResponseItem, len(request.Items)),
|
||||
}
|
||||
|
||||
for i, v := range request.Items {
|
||||
result.Items[i] = cloudmigration.MigrateDataResponseItem{
|
||||
Type: v.Type,
|
||||
RefID: v.RefID,
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
// simulate flakiness on one random item
|
||||
i := rand.Intn(len(result.Items))
|
||||
failedItem := result.Items[i]
|
||||
failedItem.Status, failedItem.Error = cloudmigration.ItemStatusError, "simulated random error"
|
||||
result.Items[i] = failedItem
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user