CloudMigration - Display different error messages for create migration errors (#94683)

* start on tokens

* more error messages

* more handling

* rephrased with suggestions from Daniel

* separate gms parse method

* use translation

* refactor initial idea to use error obj

* use error dto result

* handle gms client

* clean logs and comments

* fix tests

* tests for gms

* test and lint

* lint

* one more handling from gms

* typing in fe

* use error interface

* use validation error

* remove unused gms error

* use errorlib and helper function in fe

* regen api

* use same error util

* one more error to handle
This commit is contained in:
Dana Axinte
2024-10-21 09:45:54 +01:00
committed by GitHub
parent d608668335
commit 98e5048370
12 changed files with 193 additions and 36 deletions
@@ -324,7 +324,7 @@ func (s *Service) ValidateToken(ctx context.Context, cm cloudmigration.CloudMigr
defer span.End()
if err := s.gmsClient.ValidateKey(ctx, cm); err != nil {
return fmt.Errorf("validating token: %w", err)
return err
}
return nil
@@ -398,22 +398,22 @@ func (s *Service) CreateSession(ctx context.Context, cmd cloudmigration.CloudMig
base64Token := cmd.AuthToken
b, err := base64.StdEncoding.DecodeString(base64Token)
if err != nil {
return nil, fmt.Errorf("token could not be decoded")
return nil, cloudmigration.ErrTokenInvalid.Errorf("token could not be decoded")
}
var token cloudmigration.Base64EncodedTokenPayload
if err := json.Unmarshal(b, &token); err != nil {
return nil, fmt.Errorf("invalid token") // don't want to leak info here
return nil, cloudmigration.ErrTokenInvalid.Errorf("token could not be decoded") // don't want to leak info here
}
migration := token.ToMigration()
// validate token against GMS before saving
if err := s.ValidateToken(ctx, migration); err != nil {
return nil, fmt.Errorf("token validation: %w", err)
return nil, err
}
cm, err := s.store.CreateMigrationSession(ctx, migration)
if err != nil {
return nil, fmt.Errorf("error creating migration: %w", err)
return nil, cloudmigration.ErrSessionCreationFailure.Errorf("error creating migration")
}
s.report(ctx, &migration, gmsclient.EventConnect, 0, nil)
@@ -26,7 +26,7 @@ func (s *NoopServiceImpl) DeleteToken(ctx context.Context, uid string) error {
}
func (s *NoopServiceImpl) ValidateToken(ctx context.Context, cm cloudmigration.CloudMigrationSession) error {
return cloudmigration.ErrFeatureDisabledError
return cloudmigration.ErrMigrationDisabled
}
func (s *NoopServiceImpl) GetSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) {
@@ -38,7 +38,7 @@ func (s *NoopServiceImpl) GetSessionList(ctx context.Context) (*cloudmigration.C
}
func (s *NoopServiceImpl) CreateSession(ctx context.Context, cm cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) {
return nil, cloudmigration.ErrFeatureDisabledError
return nil, cloudmigration.ErrMigrationDisabled
}
func (s *NoopServiceImpl) DeleteSession(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) {
@@ -47,7 +47,7 @@ func (m FakeServiceImpl) DeleteToken(_ context.Context, _ string) error {
func (m FakeServiceImpl) CreateSession(_ context.Context, _ cloudmigration.CloudMigrationSessionRequest) (*cloudmigration.CloudMigrationSessionResponse, error) {
if m.ReturnError {
return nil, fmt.Errorf("mock error")
return nil, cloudmigration.ErrSessionCreationFailure
}
return &cloudmigration.CloudMigrationSessionResponse{
UID: "fake_uid",
@@ -62,3 +62,14 @@ const (
EventStartUploadingSnapshot LocalEventType = "start_uploading_snapshot"
EventDoneUploadingSnapshot LocalEventType = "done_uploading_snapshot"
)
type GMSAPIError struct {
Message string `json:"message"`
}
// Error messages returned from GMS
var (
GMSErrorMessageInstanceUnreachable = "instance is unreachable"
GMSErrorMessageInstanceCheckingError = "checking if instance is reachable"
GMSErrorMessageInstanceFetching = "fetching instance by stack id"
)
@@ -49,7 +49,7 @@ func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.Cloud
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(nil))
if err != nil {
c.log.Error("error creating http request for token validation", "err", err.Error())
return fmt.Errorf("http request error: %w", err)
return cloudmigration.ErrTokenRequestError.Errorf("create http request error")
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", cm.StackID, cm.AuthToken))
@@ -57,17 +57,21 @@ func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.Cloud
resp, err := c.httpClient.Do(req)
if err != nil {
c.log.Error("error sending http request for token validation", "err", err.Error())
return fmt.Errorf("http request error: %w", err)
return cloudmigration.ErrTokenRequestError.Errorf("send http request error")
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("closing response body: %w", closeErr))
c.log.Error("error closing the request body", "err", err.Error())
err = errors.Join(err, cloudmigration.ErrTokenRequestError.Errorf("closing response body"))
}
}()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("token validation failure: %v", string(body))
if gmsErr := c.handleGMSErrors(body); gmsErr != nil {
return gmsErr
}
return cloudmigration.ErrTokenValidationFailure.Errorf("token validation failure")
}
return nil
@@ -258,3 +262,22 @@ func (c *gmsClientImpl) buildBasePath(clusterSlug string) string {
}
return fmt.Sprintf("https://cms-%s.%s/cloud-migrations", clusterSlug, domain)
}
// handleGMSErrors parses the error message from GMS and translates it to an appropriate error message
// use ErrTokenValidationFailure for any errors which are not specifically handled
func (c *gmsClientImpl) handleGMSErrors(responseBody []byte) error {
var apiError GMSAPIError
if err := json.Unmarshal(responseBody, &apiError); err != nil {
return cloudmigration.ErrTokenValidationFailure.Errorf("token validation failure")
}
if strings.Contains(apiError.Message, GMSErrorMessageInstanceUnreachable) {
return cloudmigration.ErrInstanceUnreachable.Errorf("instance unreachable")
} else if strings.Contains(apiError.Message, GMSErrorMessageInstanceCheckingError) {
return cloudmigration.ErrInstanceRequestError.Errorf("instance checking error")
} else if strings.Contains(apiError.Message, GMSErrorMessageInstanceFetching) {
return cloudmigration.ErrInstanceRequestError.Errorf("fetching instance")
}
return cloudmigration.ErrTokenValidationFailure.Errorf("token validation failure")
}
@@ -4,6 +4,7 @@ import (
"net/http"
"testing"
"github.com/grafana/grafana/pkg/services/cloudmigration"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -65,3 +66,48 @@ func Test_buildBasePath(t *testing.T) {
})
}
}
func Test_handleGMSErrors(t *testing.T) {
t.Parallel()
c, err := NewGMSClient(&setting.Cfg{
CloudMigration: setting.CloudMigrationSettings{
GMSDomain: "http://some-domain:8080",
},
},
http.DefaultClient,
)
require.NoError(t, err)
client := c.(*gmsClientImpl)
testscases := []struct {
gmsResBody []byte
expectedError error
}{
{
gmsResBody: []byte(`{"message":"instance is unreachable, make sure the instance is running"}`),
expectedError: cloudmigration.ErrInstanceUnreachable,
},
{
gmsResBody: []byte(`{"message":"checking if instance is reachable"}`),
expectedError: cloudmigration.ErrInstanceRequestError,
},
{
gmsResBody: []byte(`{"message":"fetching instance by stack id 1234"}`),
expectedError: cloudmigration.ErrInstanceRequestError,
},
{
gmsResBody: []byte(`{"status":"error","error":"authentication error: invalid token"}`),
expectedError: cloudmigration.ErrTokenValidationFailure,
},
{
gmsResBody: []byte(""),
expectedError: cloudmigration.ErrTokenValidationFailure,
},
}
for _, tc := range testscases {
resError := client.handleGMSErrors(tc.gmsResBody)
require.ErrorIs(t, resError, tc.expectedError)
}
}
+10
View File
@@ -254,3 +254,13 @@ const (
SnapshotStateError SnapshotState = "ERROR"
SnapshotStateUnknown SnapshotState = "UNKNOWN"
)
var (
ErrTokenInvalid = errutil.Internal("cloudmigrations.createMigration.tokenInvalid", errutil.WithPublicMessage("Token is not valid. Generate a new token on your cloud instance and try again."))
ErrTokenRequestError = errutil.Internal("cloudmigrations.createMigration.tokenRequestError", errutil.WithPublicMessage("An error occurred while validating the token. Please check the Grafana instance logs."))
ErrTokenValidationFailure = errutil.Internal("cloudmigrations.createMigration.tokenValidationFailure", errutil.WithPublicMessage("Token is not valid. Please ensure the token matches the migration token on your cloud instance."))
ErrInstanceUnreachable = errutil.Internal("cloudmigrations.createMigration.instanceUnreachable", errutil.WithPublicMessage("The cloud instance cannot be reached. Make sure the instance is running and try again."))
ErrInstanceRequestError = errutil.Internal("cloudmigrations.createMigration.instanceRequestError", errutil.WithPublicMessage("An error occurred while attempting to verify the cloud instance's connectivity. Please check the network settings or cloud instance status."))
ErrSessionCreationFailure = errutil.Internal("cloudmigrations.createMigration.sessionCreationFailure", errutil.WithPublicMessage("There was an error creating the migration. Please try again."))
ErrMigrationDisabled = errutil.Internal("cloudmigrations.createMigration.migrationDisabled", errutil.WithPublicMessage("Cloud migrations are disabled on this instance."))
)