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
@@ -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)
}
}