mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
d608668335
commit
98e5048370
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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."))
|
||||
)
|
||||
|
19
public/app/features/migrate-to-cloud/api/errors.ts
Normal file
19
public/app/features/migrate-to-cloud/api/errors.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
|
||||
// TODO: candidate to hoist and share
|
||||
export function maybeAPIError(err: unknown) {
|
||||
if (!isFetchError<unknown>(err) || typeof err.data !== 'object' || !err.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = err?.data;
|
||||
const message = 'message' in data && typeof data.message === 'string' ? data.message : null;
|
||||
const messageId = 'messageId' in data && typeof data.messageId === 'string' ? data.messageId : null;
|
||||
const statusCode = 'statusCode' in data && typeof data.statusCode === 'number' ? data.statusCode : null;
|
||||
|
||||
if (!message || !messageId || !statusCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { message, messageId, statusCode };
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { isFetchError, reportInteraction } from '@grafana/runtime';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Box, Button, Text } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
@ -9,30 +9,13 @@ import {
|
||||
useDeleteCloudMigrationTokenMutation,
|
||||
useGetCloudMigrationTokenQuery,
|
||||
} from '../../api';
|
||||
import { maybeAPIError } from '../../api/errors';
|
||||
import { TokenErrorAlert } from '../TokenErrorAlert';
|
||||
|
||||
import { CreateTokenModal } from './CreateTokenModal';
|
||||
import { DeleteTokenConfirmationModal } from './DeleteTokenConfirmationModal';
|
||||
import { TokenStatus } from './TokenStatus';
|
||||
|
||||
// TODO: candidate to hoist and share
|
||||
function maybeAPIError(err: unknown) {
|
||||
if (!isFetchError<unknown>(err) || typeof err.data !== 'object' || !err.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = err?.data;
|
||||
const message = 'message' in data && typeof data.message === 'string' ? data.message : null;
|
||||
const messageId = 'messageId' in data && typeof data.messageId === 'string' ? data.messageId : null;
|
||||
const statusCode = 'statusCode' in data && typeof data.statusCode === 'number' ? data.statusCode : null;
|
||||
|
||||
if (!message || !messageId || !statusCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { message, messageId, statusCode };
|
||||
}
|
||||
|
||||
export const MigrationTokenPane = () => {
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
@ -8,6 +8,7 @@ import { Trans, t } from 'app/core/internationalization';
|
||||
import { AlertWithTraceID } from 'app/features/migrate-to-cloud/shared/AlertWithTraceID';
|
||||
|
||||
import { CreateSessionApiArg } from '../../../api';
|
||||
import { maybeAPIError } from '../../../api/errors';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
@ -21,6 +22,51 @@ interface FormData {
|
||||
token: string;
|
||||
}
|
||||
|
||||
function getTMessage(messageId: string): string {
|
||||
switch (messageId) {
|
||||
case 'cloudmigrations.createMigration.tokenInvalid':
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.token-invalid',
|
||||
'Token is not valid. Generate a new token on your cloud instance and try again.'
|
||||
);
|
||||
case 'cloudmigrations.createMigration.tokenRequestError':
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.token-request-error',
|
||||
'An error occurred while validating the token. Please check the Grafana instance logs.'
|
||||
);
|
||||
case 'cloudmigrations.createMigration.tokenValidationFailure':
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.token-validation-failure',
|
||||
'Token is not valid. Please ensure the token matches the migration token on your cloud instance.'
|
||||
);
|
||||
case 'cloudmigrations.createMigration.instanceUnreachable':
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.instance-unreachable',
|
||||
'The cloud instance cannot be reached. Make sure the instance is running and try again.'
|
||||
);
|
||||
case 'cloudmigrations.createMigration.instanceRequestError':
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.instance-request-error',
|
||||
"An error occurred while attempting to verify the cloud instance's connectivity. Please check the network settings or cloud instance status."
|
||||
);
|
||||
case 'cloudmigrations.createMigration.sessionCreationFailure':
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.session-creation-failure',
|
||||
'There was an error creating the migration. Please try again.'
|
||||
);
|
||||
case 'cloudmigrations.createMigration.migrationDisabled':
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.migration-disabled',
|
||||
'Cloud migrations are disabled on this instance.'
|
||||
);
|
||||
default:
|
||||
return t(
|
||||
'migrate-to-cloud.connect-modal.token-errors.token-not-saved',
|
||||
'There was an error saving the token. See the Grafana server logs for more details.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ConnectModal = ({ isOpen, isLoading, error, hideModal, onConfirm }: Props) => {
|
||||
const tokenId = useId();
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -101,9 +147,10 @@ export const ConnectModal = ({ isOpen, isLoading, error, hideModal, onConfirm }:
|
||||
severity="error"
|
||||
title={t('migrate-to-cloud.connect-modal.token-error-title', 'Error saving token')}
|
||||
>
|
||||
<Trans i18nKey="migrate-to-cloud.connect-modal.token-error-description">
|
||||
There was an error saving the token. See the Grafana server logs for more details.
|
||||
</Trans>
|
||||
<Text element="p">
|
||||
{getTMessage(maybeAPIError(error)?.messageId || '') ||
|
||||
'There was an error saving the token. See the Grafana server logs for more details.'}
|
||||
</Text>
|
||||
</AlertWithTraceID>
|
||||
) : undefined}
|
||||
|
||||
|
@ -1402,8 +1402,17 @@
|
||||
"connect": "Connect to this stack",
|
||||
"connecting": "Connecting to this stack...",
|
||||
"title": "Connect to a cloud stack",
|
||||
"token-error-description": "There was an error saving the token. See the Grafana server logs for more details.",
|
||||
"token-error-title": "Error saving token",
|
||||
"token-errors": {
|
||||
"instance-request-error": "An error occurred while attempting to verify the cloud instance's connectivity. Please check the network settings or cloud instance status.",
|
||||
"instance-unreachable": "The cloud instance cannot be reached. Make sure the instance is running and try again.",
|
||||
"migration-disabled": "Cloud migrations are disabled on this instance.",
|
||||
"session-creation-failure": "There was an error creating the migration. Please try again.",
|
||||
"token-invalid": "Token is not valid. Generate a new token on your cloud instance and try again.",
|
||||
"token-not-saved": "There was an error saving the token. See the Grafana server logs for more details.",
|
||||
"token-request-error": "An error occurred while validating the token. Please check the Grafana instance logs.",
|
||||
"token-validation-failure": "Token is not valid. Please ensure the token matches the migration token on your cloud instance."
|
||||
},
|
||||
"token-required-error": "Migration token is required"
|
||||
},
|
||||
"cta": {
|
||||
|
@ -1402,8 +1402,17 @@
|
||||
"connect": "Cőʼnʼnęčŧ ŧő ŧĥįş şŧäčĸ",
|
||||
"connecting": "Cőʼnʼnęčŧįʼnģ ŧő ŧĥįş şŧäčĸ...",
|
||||
"title": "Cőʼnʼnęčŧ ŧő ä čľőūđ şŧäčĸ",
|
||||
"token-error-description": "Ŧĥęřę ŵäş äʼn ęřřőř şävįʼnģ ŧĥę ŧőĸęʼn. Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||
"token-error-title": "Ēřřőř şävįʼnģ ŧőĸęʼn",
|
||||
"token-errors": {
|
||||
"instance-request-error": "Åʼn ęřřőř őččūřřęđ ŵĥįľę äŧŧęmpŧįʼnģ ŧő vęřįƒy ŧĥę čľőūđ įʼnşŧäʼnčę'ş čőʼnʼnęčŧįvįŧy. Pľęäşę čĥęčĸ ŧĥę ʼnęŧŵőřĸ şęŧŧįʼnģş őř čľőūđ įʼnşŧäʼnčę şŧäŧūş.",
|
||||
"instance-unreachable": "Ŧĥę čľőūđ įʼnşŧäʼnčę čäʼnʼnőŧ þę řęäčĥęđ. Mäĸę şūřę ŧĥę įʼnşŧäʼnčę įş řūʼnʼnįʼnģ äʼnđ ŧřy äģäįʼn.",
|
||||
"migration-disabled": "Cľőūđ mįģřäŧįőʼnş äřę đįşäþľęđ őʼn ŧĥįş įʼnşŧäʼnčę.",
|
||||
"session-creation-failure": "Ŧĥęřę ŵäş äʼn ęřřőř čřęäŧįʼnģ ŧĥę mįģřäŧįőʼn. Pľęäşę ŧřy äģäįʼn.",
|
||||
"token-invalid": "Ŧőĸęʼn įş ʼnőŧ väľįđ. Ğęʼnęřäŧę ä ʼnęŵ ŧőĸęʼn őʼn yőūř čľőūđ įʼnşŧäʼnčę äʼnđ ŧřy äģäįʼn.",
|
||||
"token-not-saved": "Ŧĥęřę ŵäş äʼn ęřřőř şävįʼnģ ŧĥę ŧőĸęʼn. Ŝęę ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.",
|
||||
"token-request-error": "Åʼn ęřřőř őččūřřęđ ŵĥįľę väľįđäŧįʼnģ ŧĥę ŧőĸęʼn. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä įʼnşŧäʼnčę ľőģş.",
|
||||
"token-validation-failure": "Ŧőĸęʼn įş ʼnőŧ väľįđ. Pľęäşę ęʼnşūřę ŧĥę ŧőĸęʼn mäŧčĥęş ŧĥę mįģřäŧįőʼn ŧőĸęʼn őʼn yőūř čľőūđ įʼnşŧäʼnčę."
|
||||
},
|
||||
"token-required-error": "Mįģřäŧįőʼn ŧőĸęʼn įş řęqūįřęđ"
|
||||
},
|
||||
"cta": {
|
||||
|
Loading…
Reference in New Issue
Block a user