CloudMigrations: Query Grafana Migration Status for status while the snapshot is in the cloud (#90314)
* implement querying gms for snapshot status * add some documentation * provide snapshot resources after snapshot is created * add rate limiting to backend * fix compilation error * fix typo * add unit tests * finish merge * lint * swagger gen * more testing * remove duplicate test * address a couple PR comments * update switch statement to a map * add timeouts to gms client through the http client * remove extra whitespace * put method back where it was so the PR is less confusing * fix tests * add todo * fix final unit test
This commit is contained in:
@@ -10,7 +10,7 @@ type Client interface {
|
||||
ValidateKey(context.Context, cloudmigration.CloudMigrationSession) error
|
||||
MigrateData(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.MigrateDataRequest) (*cloudmigration.MigrateDataResponse, error)
|
||||
StartSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error)
|
||||
GetSnapshotStatus(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot) (*cloudmigration.CloudMigrationSnapshot, error)
|
||||
GetSnapshotStatus(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot) (*cloudmigration.GetSnapshotStatusResponse, error)
|
||||
}
|
||||
|
||||
const logPrefix = "cloudmigration.gmsclient"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// TODO: Move these to a shared library in common with GMS
|
||||
package gmsclient
|
||||
|
||||
type MigrateDataType string
|
||||
|
||||
@@ -9,29 +9,38 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// NewGMSClient returns an implementation of Client that queries GrafanaMigrationService
|
||||
func NewGMSClient(domain string) Client {
|
||||
return &gmsClientImpl{
|
||||
domain: domain,
|
||||
log: log.New(logPrefix),
|
||||
func NewGMSClient(cfg *setting.Cfg) (Client, error) {
|
||||
if cfg.CloudMigration.GMSDomain == "" {
|
||||
return nil, fmt.Errorf("missing GMS domain")
|
||||
}
|
||||
return &gmsClientImpl{
|
||||
cfg: cfg,
|
||||
log: log.New(logPrefix),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type gmsClientImpl struct {
|
||||
domain string
|
||||
log *log.ConcreteLogger
|
||||
cfg *setting.Cfg
|
||||
log *log.ConcreteLogger
|
||||
|
||||
getStatusMux sync.Mutex
|
||||
getStatusLastQueried time.Time
|
||||
}
|
||||
|
||||
func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.CloudMigrationSession) (err error) {
|
||||
logger := c.log.FromContext(ctx)
|
||||
|
||||
// TODO update service url to gms
|
||||
path := fmt.Sprintf("%s/api/v1/validate-key", buildBasePath(c.domain, cm.ClusterSlug))
|
||||
// TODO: there is a lot of boilerplate code in these methods, we should consolidate them when we have a gardening period
|
||||
path := fmt.Sprintf("%s/api/v1/validate-key", c.buildBasePath(cm.ClusterSlug))
|
||||
|
||||
// validation is an empty POST to GMS with the authorization header included
|
||||
req, err := http.NewRequest("POST", path, bytes.NewReader(nil))
|
||||
@@ -42,7 +51,9 @@ func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.Cloud
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", cm.StackID, cm.AuthToken))
|
||||
|
||||
client := &http.Client{}
|
||||
client := &http.Client{
|
||||
Timeout: c.cfg.CloudMigration.GMSValidateKeyTimeout,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Error("error sending http request for token validation", "err", err.Error())
|
||||
@@ -62,11 +73,12 @@ func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.Cloud
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
func (c *gmsClientImpl) MigrateData(ctx context.Context, cm cloudmigration.CloudMigrationSession, request cloudmigration.MigrateDataRequest) (result *cloudmigration.MigrateDataResponse, err error) {
|
||||
logger := c.log.FromContext(ctx)
|
||||
|
||||
// TODO update service url to gms
|
||||
path := fmt.Sprintf("%s/api/v1/migrate-data", buildBasePath(c.domain, cm.ClusterSlug))
|
||||
path := fmt.Sprintf("%s/api/v1/migrate-data", c.buildBasePath(cm.ClusterSlug))
|
||||
|
||||
reqDTO := convertRequestToDTO(request)
|
||||
body, err := json.Marshal(reqDTO)
|
||||
@@ -111,7 +123,7 @@ func (c *gmsClientImpl) MigrateData(ctx context.Context, cm cloudmigration.Cloud
|
||||
}
|
||||
|
||||
func (c *gmsClientImpl) StartSnapshot(ctx context.Context, session cloudmigration.CloudMigrationSession) (out *cloudmigration.StartSnapshotResponse, err error) {
|
||||
path := fmt.Sprintf("%s/api/v1/start-snapshot", buildBasePath(c.domain, session.ClusterSlug))
|
||||
path := fmt.Sprintf("%s/api/v1/start-snapshot", c.buildBasePath(session.ClusterSlug))
|
||||
|
||||
// Send the request to cms with the associated auth token
|
||||
req, err := http.NewRequest(http.MethodPost, path, nil)
|
||||
@@ -122,7 +134,9 @@ func (c *gmsClientImpl) StartSnapshot(ctx context.Context, session cloudmigratio
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", session.StackID, session.AuthToken))
|
||||
|
||||
client := &http.Client{}
|
||||
client := &http.Client{
|
||||
Timeout: c.cfg.CloudMigration.GMSStartSnapshotTimeout,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.log.Error("error sending http request to start snapshot", "err", err.Error())
|
||||
@@ -148,8 +162,56 @@ func (c *gmsClientImpl) StartSnapshot(ctx context.Context, session cloudmigratio
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *gmsClientImpl) GetSnapshotStatus(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
panic("not implemented")
|
||||
func (c *gmsClientImpl) GetSnapshotStatus(ctx context.Context, session cloudmigration.CloudMigrationSession, snapshot cloudmigration.CloudMigrationSnapshot) (*cloudmigration.GetSnapshotStatusResponse, error) {
|
||||
c.getStatusMux.Lock()
|
||||
defer c.getStatusMux.Unlock()
|
||||
logger := c.log.FromContext(ctx)
|
||||
|
||||
path := fmt.Sprintf("%s/api/v1/status/%s/status", c.buildBasePath(session.ClusterSlug), snapshot.GMSSnapshotUID)
|
||||
|
||||
// Send the request to gms with the associated auth token
|
||||
req, err := http.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
c.log.Error("error creating http request to get snapshot status", "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", session.StackID, session.AuthToken))
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: c.cfg.CloudMigration.GMSGetSnapshotStatusTimeout,
|
||||
}
|
||||
c.getStatusLastQueried = time.Now()
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.log.Error("error sending http request to get snapshot status", "err", err.Error())
|
||||
return nil, fmt.Errorf("http request error: %w", err)
|
||||
} else if resp.StatusCode >= 400 {
|
||||
c.log.Error("received error response to get snapshot status", "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 result cloudmigration.GetSnapshotStatusResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
logger.Error("unmarshalling response body: %w", err)
|
||||
return nil, fmt.Errorf("unmarshalling get snapshot status response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *gmsClientImpl) buildBasePath(clusterSlug string) string {
|
||||
domain := c.cfg.CloudMigration.GMSDomain
|
||||
if strings.HasPrefix(domain, "http://localhost") {
|
||||
return domain
|
||||
}
|
||||
return fmt.Sprintf("https://cms-%s.%s/cloud-migrations", clusterSlug, domain)
|
||||
}
|
||||
|
||||
func convertRequestToDTO(request cloudmigration.MigrateDataRequest) MigrateDataRequestDTO {
|
||||
@@ -185,10 +247,3 @@ func convertResponseFromDTO(result MigrateDataResponseDTO) cloudmigration.Migrat
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
func buildBasePath(domain, clusterSlug string) string {
|
||||
if strings.HasPrefix(domain, "http://localhost") {
|
||||
return domain
|
||||
}
|
||||
return fmt.Sprintf("https://cms-%s.%s/cloud-migrations", clusterSlug, domain)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,31 @@ package gmsclient
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_buildBasePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Domain is required
|
||||
_, err := NewGMSClient(&setting.Cfg{
|
||||
CloudMigration: setting.CloudMigrationSettings{
|
||||
GMSDomain: "",
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
// Domain is required
|
||||
c, err := NewGMSClient(&setting.Cfg{
|
||||
CloudMigration: setting.CloudMigrationSettings{
|
||||
GMSDomain: "non-empty",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client := c.(*gmsClientImpl)
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
domain string
|
||||
@@ -30,7 +49,8 @@ func Test_buildBasePath(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, buildBasePath(tt.domain, tt.clusterSlug))
|
||||
client.cfg.CloudMigration.GMSDomain = tt.domain
|
||||
assert.Equal(t, tt.expected, client.buildBasePath(tt.clusterSlug))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
cryptoRand "crypto/rand"
|
||||
|
||||
@@ -68,33 +67,28 @@ func (c *memoryClientImpl) StartSnapshot(context.Context, cloudmigration.CloudMi
|
||||
return c.snapshot, nil
|
||||
}
|
||||
|
||||
func (c *memoryClientImpl) GetSnapshotStatus(ctx context.Context, session cloudmigration.CloudMigrationSession, snapshot cloudmigration.CloudMigrationSnapshot) (*cloudmigration.CloudMigrationSnapshot, error) {
|
||||
results := []cloudmigration.CloudMigrationResource{
|
||||
{
|
||||
Type: cloudmigration.DashboardDataType,
|
||||
RefID: "dash1",
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
},
|
||||
{
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: "ds1",
|
||||
Status: cloudmigration.ItemStatusError,
|
||||
Error: "fake error",
|
||||
},
|
||||
{
|
||||
Type: cloudmigration.FolderDataType,
|
||||
RefID: "folder1",
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
func (c *memoryClientImpl) GetSnapshotStatus(ctx context.Context, session cloudmigration.CloudMigrationSession, snapshot cloudmigration.CloudMigrationSnapshot) (*cloudmigration.GetSnapshotStatusResponse, error) {
|
||||
gmsResp := &cloudmigration.GetSnapshotStatusResponse{
|
||||
State: cloudmigration.SnapshotStateFinished,
|
||||
Results: []cloudmigration.CloudMigrationResource{
|
||||
{
|
||||
Type: cloudmigration.DashboardDataType,
|
||||
RefID: "dash1",
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
},
|
||||
{
|
||||
Type: cloudmigration.DatasourceDataType,
|
||||
RefID: "ds1",
|
||||
Status: cloudmigration.ItemStatusError,
|
||||
Error: "fake error",
|
||||
},
|
||||
{
|
||||
Type: cloudmigration.FolderDataType,
|
||||
RefID: "folder1",
|
||||
Status: cloudmigration.ItemStatusOK,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// just fake an entire response
|
||||
gmsSnapshot := cloudmigration.CloudMigrationSnapshot{
|
||||
Status: cloudmigration.SnapshotStatusFinished,
|
||||
GMSSnapshotUID: "gmssnapshotuid",
|
||||
Resources: results,
|
||||
Finished: time.Now(),
|
||||
}
|
||||
|
||||
return &gmsSnapshot, nil
|
||||
return gmsResp, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user