diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index 338ee41f911..fde10066063 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -390,6 +390,7 @@ func (cma *CloudMigrationAPI) GetSnapshot(c *contextmodel.ReqContext) response.R RefID: results[i].RefID, Status: ItemStatus(results[i].Status), Message: results[i].Error, + ErrorCode: ItemErrorCode(results[i].ErrorCode), ParentName: results[i].ParentName, } } diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index 0e49d253c57..b64d7057778 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -113,8 +113,9 @@ type MigrateDataResponseItemDTO struct { // required:true RefID string `json:"refId"` // required:true - Status ItemStatus `json:"status"` - Message string `json:"message,omitempty"` + Status ItemStatus `json:"status"` + Message string `json:"message,omitempty"` + ErrorCode ItemErrorCode `json:"errorCode,omitempty"` } // swagger:enum MigrateDataType @@ -143,6 +144,21 @@ const ( ItemStatusUnknown ItemStatus = "UNKNOWN" ) +// swagger:enum ItemErrorCode +type ItemErrorCode string + +const ( + ErrDatasourceNameConflict ItemErrorCode = "DATASOURCE_NAME_CONFLICT" + ErrDashboardAlreadyManaged ItemErrorCode = "DASHBOARD_ALREADY_MANAGED" + ErrLibraryElementNameConflict ItemErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" + ErrUnsupportedDataType ItemErrorCode = "UNSUPPORTED_DATA_TYPE" + ErrResourceConflict ItemErrorCode = "RESOURCE_CONFLICT" + ErrUnexpectedStatus ItemErrorCode = "UNEXPECTED_STATUS_CODE" + ErrInternalServiceError ItemErrorCode = "INTERNAL_SERVICE_ERROR" + ErrOnlyCoreDataSources ItemErrorCode = "ONLY_CORE_DATA_SOURCES" + ErrGeneric ItemErrorCode = "GENERIC_ERROR" +) + // swagger:parameters getCloudMigrationRun type GetMigrationRunParams struct { // RunUID of a migration run diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index bf689200719..acf9db47162 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -573,6 +573,13 @@ func (s *Service) GetSnapshot(ctx context.Context, query cloudmigration.GetSnaps s.log.Error("error applying plugin warnings, please open a bug report: %w", err) } + // Log the errors for resources with errors at migration + for _, resource := range resources { + if resource.Status == cloudmigration.ItemStatusError && resource.Error != "" { + s.log.Error("Could not migrate resource", "resourceID", resource.RefID, "error", resource.Error) + } + } + // We need to update the snapshot in our db before reporting anything if err := s.store.UpdateSnapshot(ctx, cloudmigration.UpdateSnapshotCmd{ UID: snapshot.UID, @@ -832,6 +839,7 @@ func (s *Service) getResourcesWithPluginWarnings(ctx context.Context, results [] // if the plugin is not found, it means it was uninstalled, meaning it wasn't core if !p.IsCorePlugin() || !found { r.Status = cloudmigration.ItemStatusWarning + r.ErrorCode = cloudmigration.ErrOnlyCoreDataSources r.Error = "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack." } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index abccfbaf653..b5189fffcff 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -312,11 +312,11 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li // If the uid is not known, it uses snapshot_uid + resource_uid as a lookup func (ss *sqlStore) CreateUpdateSnapshotResources(ctx context.Context, snapshotUid string, resources []cloudmigration.CloudMigrationResource) error { return ss.db.InTransaction(ctx, func(ctx context.Context) error { - sql := "UPDATE cloud_migration_resource SET status=?, error_string=? WHERE uid=? OR (snapshot_uid=? AND resource_uid=?)" + sql := "UPDATE cloud_migration_resource SET status=?, error_string=?, error_code=? WHERE uid=? OR (snapshot_uid=? AND resource_uid=?)" err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { for _, r := range resources { // try an update first - result, err := sess.Exec(sql, r.Status, r.Error, r.UID, snapshotUid, r.RefID) + result, err := sess.Exec(sql, r.Status, r.Error, r.ErrorCode, r.UID, snapshotUid, r.RefID) if err != nil { return err } diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index d097c777826..ca52efeb63e 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -68,11 +68,12 @@ type CloudMigrationResource struct { ID int64 `xorm:"pk autoincr 'id'"` UID string `xorm:"uid"` - Name string `xorm:"name" json:"name"` - Type MigrateDataType `xorm:"resource_type" json:"type"` - RefID string `xorm:"resource_uid" json:"refId"` - Status ItemStatus `xorm:"status" json:"status"` - Error string `xorm:"error_string" json:"error"` + Name string `xorm:"name" json:"name"` + Type MigrateDataType `xorm:"resource_type" json:"type"` + RefID string `xorm:"resource_uid" json:"refId"` + Status ItemStatus `xorm:"status" json:"status"` + Error string `xorm:"error_string" json:"error"` + ErrorCode ResourceErrorCode `xorm:"error_code" json:"error_code"` SnapshotUID string `xorm:"snapshot_uid"` ParentName string `xorm:"parent_name" json:"parentName"` @@ -101,6 +102,20 @@ const ( ItemStatusPending ItemStatus = "PENDING" ) +type ResourceErrorCode string + +const ( + ErrDatasourceNameConflict ResourceErrorCode = "DATASOURCE_NAME_CONFLICT" + ErrDashboardAlreadyManaged ResourceErrorCode = "DASHBOARD_ALREADY_MANAGED" + ErrLibraryElementNameConflict ResourceErrorCode = "LIBRARY_ELEMENT_NAME_CONFLICT" + ErrUnsupportedDataType ResourceErrorCode = "UNSUPPORTED_DATA_TYPE" + ErrResourceConflict ResourceErrorCode = "RESOURCE_CONFLICT" + ErrUnexpectedStatus ResourceErrorCode = "UNEXPECTED_STATUS_CODE" + ErrInternalServiceError ResourceErrorCode = "INTERNAL_SERVICE_ERROR" + ErrOnlyCoreDataSources ResourceErrorCode = "ONLY_CORE_DATA_SOURCES" + ErrGeneric ResourceErrorCode = "GENERIC_ERROR" +) + type SnapshotResourceStats struct { CountsByType map[MigrateDataType]int CountsByStatus map[ItemStatus]int diff --git a/pkg/services/sqlstore/migrations/cloud_migrations.go b/pkg/services/sqlstore/migrations/cloud_migrations.go index 7dd4b167595..5ddebb64817 100644 --- a/pkg/services/sqlstore/migrations/cloud_migrations.go +++ b/pkg/services/sqlstore/migrations/cloud_migrations.go @@ -170,4 +170,10 @@ func addCloudMigrationsMigrations(mg *Migrator) { Type: DB_Text, Nullable: true, })) + + mg.AddMigration("add cloud_migration_resource.error_code column", NewAddColumnMigration(migrationResourceTable, &Column{ + Name: "error_code", + Type: DB_Text, + Nullable: true, + })) } diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 377ab63e132..ff82b23fbd2 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5769,6 +5769,20 @@ "status" ], "properties": { + "errorCode": { + "type": "string", + "enum": [ + "DATASOURCE_NAME_CONFLICT", + "DASHBOARD_ALREADY_MANAGED", + "LIBRARY_ELEMENT_NAME_CONFLICT", + "UNSUPPORTED_DATA_TYPE", + "RESOURCE_CONFLICT", + "UNEXPECTED_STATUS_CODE", + "INTERNAL_SERVICE_ERROR", + "ONLY_CORE_DATA_SOURCES", + "GENERIC_ERROR" + ] + }, "message": { "type": "string" }, diff --git a/public/api-merged.json b/public/api-merged.json index 8395ffc2a35..6b2f22e8d89 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -17262,6 +17262,20 @@ "status" ], "properties": { + "errorCode": { + "type": "string", + "enum": [ + "DATASOURCE_NAME_CONFLICT", + "DASHBOARD_ALREADY_MANAGED", + "LIBRARY_ELEMENT_NAME_CONFLICT", + "UNSUPPORTED_DATA_TYPE", + "RESOURCE_CONFLICT", + "UNEXPECTED_STATUS_CODE", + "INTERNAL_SERVICE_ERROR", + "ONLY_CORE_DATA_SOURCES", + "GENERIC_ERROR" + ] + }, "message": { "type": "string" }, diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts index f5a589293e5..bba187c2509 100644 --- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts +++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts @@ -164,6 +164,16 @@ export type CreateSnapshotResponseDto = { uid?: string; }; export type MigrateDataResponseItemDto = { + errorCode?: + | 'DATASOURCE_NAME_CONFLICT' + | 'DASHBOARD_ALREADY_MANAGED' + | 'LIBRARY_ELEMENT_NAME_CONFLICT' + | 'UNSUPPORTED_DATA_TYPE' + | 'RESOURCE_CONFLICT' + | 'UNEXPECTED_STATUS_CODE' + | 'INTERNAL_SERVICE_ERROR' + | 'ONLY_CORE_DATA_SOURCES' + | 'GENERIC_ERROR'; message?: string; name?: string; parentName?: string; diff --git a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx index adc673b9ecd..d8e57f11835 100644 --- a/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx +++ b/public/app/features/migrate-to-cloud/onprem/ResourceDetailsModal.tsx @@ -1,6 +1,8 @@ import { Button, Modal, Stack, Text } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; +import { MigrateDataResponseItemDto } from '../api'; + import { prettyTypeName } from './TypeCell'; import { ResourceTableItem } from './types'; @@ -9,11 +11,65 @@ interface ResourceDetailsModalProps { onClose: () => void; } +function getTMessage(errorCode: MigrateDataResponseItemDto['errorCode']): string { + switch (errorCode) { + case 'DATASOURCE_NAME_CONFLICT': + return t( + 'migrate-to-cloud.resource-details.error-messages.datasource-name-conflict', + 'There is a data source with the same name in the target instance. Rename one of them and try again.' + ); + case 'DASHBOARD_ALREADY_MANAGED': + return t( + 'migrate-to-cloud.resource-details.error-messages.dashboard-already-managed', + 'Dashboard is already provisioned and managed by Grafana in the cloud instance. We recommend using the provisioned dashboard going forward. If you still wish to copy the dashboard to the cloud instance, then change the dashboard ID in the dashboard JSON, save a new snapshot and upload again.' + ); + case 'LIBRARY_ELEMENT_NAME_CONFLICT': + return t( + 'migrate-to-cloud.resource-details.error-messages.library-element-name-conflict', + 'There is a library element with the same name in the target instance. Rename one of them and try again.' + ); + case 'UNSUPPORTED_DATA_TYPE': + return t( + 'migrate-to-cloud.resource-details.error-messages.unsupported-data-type', + 'Migration of this data type is not currently supported.' + ); + case 'RESOURCE_CONFLICT': + return t( + 'migrate-to-cloud.resource-details.error-messages.resource-conflict', + 'There is a resource conflict with the target instance. Please check the Grafana server logs for more details.' + ); + case 'ONLY_CORE_DATA_SOURCES': + return t( + 'migrate-to-cloud.resource-details.error-messages.only-core-data-sources', + 'Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.' + ); + case 'UNEXPECTED_STATUS_CODE': + return t( + 'migrate-to-cloud.resource-details.error-messages.unexpected-error', + 'There has been an error while migrating. Please check the Grafana server logs for more details.' + ); + case 'INTERNAL_SERVICE_ERROR': + return t( + 'migrate-to-cloud.resource-details.error-messages.internal-service-error', + 'There has been an error while migrating. Please check the Grafana server logs for more details.' + ); + case 'GENERIC_ERROR': + return t( + 'migrate-to-cloud.resource-details.error-messages.generic-error', + 'There has been an error while migrating. Please check the cloud migration logs for more information.' + ); + // Handle new errors here + default: + return ''; + } +} + export function ResourceDetailsModal(props: ResourceDetailsModalProps) { const { resource, onClose } = props; const refId = resource?.refId; const typeName = resource && prettyTypeName(resource.type); + const hasError = resource?.errorCode || resource?.message; let msgTitle = t('migrate-to-cloud.resource-details.generic-title', 'Resource migration details:'); if (resource?.status === 'ERROR') { @@ -36,12 +92,13 @@ export function ResourceDetailsModal(props: ResourceDetailsModalProps) { - {resource.message ? ( + {hasError ? ( <> {msgTitle} - - - {resource.message} + + {getTMessage(resource?.errorCode) || + resource?.message || + 'There has been an error while migrating. Please check the cloud migration logs for more information.'} ) : ( diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 780b37f7ab6..a376481e1b2 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1472,6 +1472,17 @@ }, "resource-details": { "dismiss-button": "OK", + "error-messages": { + "dashboard-already-managed": "Dashboard is already provisioned and managed by Grafana in the cloud instance. We recommend using the provisioned dashboard going forward. If you still wish to copy the dashboard to the cloud instance, then change the dashboard ID in the dashboard JSON, save a new snapshot and upload again.", + "datasource-name-conflict": "There is a data source with the same name in the target instance. Rename one of them and try again.", + "generic-error": "There has been an error while migrating. Please check the cloud migration logs for more information.", + "internal-service-error": "There has been an error while migrating. Please check the Grafana server logs for more details.", + "library-element-name-conflict": "There is a library element with the same name in the target instance. Rename one of them and try again.", + "only-core-data-sources": "Only core data sources are supported. Please ensure the plugin is installed on the cloud stack.", + "resource-conflict": "There is a resource conflict with the target instance. Please check the Grafana server logs for more details.", + "unexpected-error": "There has been an error while migrating. Please check the Grafana server logs for more details.", + "unsupported-data-type": "Migration of this data type is not currently supported." + }, "error-title": "Unable to migrate this resource:", "generic-title": "Resource migration details:", "missing-message": "No message provided.", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 4b10a1e4a89..3b372bf796f 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1472,6 +1472,17 @@ }, "resource-details": { "dismiss-button": "ØĶ", + "error-messages": { + "dashboard-already-managed": "Đäşĥþőäřđ įş äľřęäđy přővįşįőʼnęđ äʼnđ mäʼnäģęđ þy Ğřäƒäʼnä įʼn ŧĥę čľőūđ įʼnşŧäʼnčę. Ŵę řęčőmmęʼnđ ūşįʼnģ ŧĥę přővįşįőʼnęđ đäşĥþőäřđ ģőįʼnģ ƒőřŵäřđ. Ĩƒ yőū şŧįľľ ŵįşĥ ŧő čőpy ŧĥę đäşĥþőäřđ ŧő ŧĥę čľőūđ įʼnşŧäʼnčę, ŧĥęʼn čĥäʼnģę ŧĥę đäşĥþőäřđ ĨĐ įʼn ŧĥę đäşĥþőäřđ ĴŜØŃ, şävę ä ʼnęŵ şʼnäpşĥőŧ äʼnđ ūpľőäđ äģäįʼn.", + "datasource-name-conflict": "Ŧĥęřę įş ä đäŧä şőūřčę ŵįŧĥ ŧĥę şämę ʼnämę įʼn ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Ŗęʼnämę őʼnę őƒ ŧĥęm äʼnđ ŧřy äģäįʼn.", + "generic-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę čľőūđ mįģřäŧįőʼn ľőģş ƒőř mőřę įʼnƒőřmäŧįőʼn.", + "internal-service-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.", + "library-element-name-conflict": "Ŧĥęřę įş ä ľįþřäřy ęľęmęʼnŧ ŵįŧĥ ŧĥę şämę ʼnämę įʼn ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Ŗęʼnämę őʼnę őƒ ŧĥęm äʼnđ ŧřy äģäįʼn.", + "only-core-data-sources": "Øʼnľy čőřę đäŧä şőūřčęş äřę şūppőřŧęđ. Pľęäşę ęʼnşūřę ŧĥę pľūģįʼn įş įʼnşŧäľľęđ őʼn ŧĥę čľőūđ şŧäčĸ.", + "resource-conflict": "Ŧĥęřę įş ä řęşőūřčę čőʼnƒľįčŧ ŵįŧĥ ŧĥę ŧäřģęŧ įʼnşŧäʼnčę. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.", + "unexpected-error": "Ŧĥęřę ĥäş þęęʼn äʼn ęřřőř ŵĥįľę mįģřäŧįʼnģ. Pľęäşę čĥęčĸ ŧĥę Ğřäƒäʼnä şęřvęř ľőģş ƒőř mőřę đęŧäįľş.", + "unsupported-data-type": "Mįģřäŧįőʼn őƒ ŧĥįş đäŧä ŧypę įş ʼnőŧ čūřřęʼnŧľy şūppőřŧęđ." + }, "error-title": "Ůʼnäþľę ŧő mįģřäŧę ŧĥįş řęşőūřčę:", "generic-title": "Ŗęşőūřčę mįģřäŧįőʼn đęŧäįľş:", "missing-message": "Ńő męşşäģę přővįđęđ.", diff --git a/public/openapi3.json b/public/openapi3.json index 06a39b22e73..9424c6bbccb 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -7215,6 +7215,20 @@ }, "MigrateDataResponseItemDTO": { "properties": { + "errorCode": { + "enum": [ + "DATASOURCE_NAME_CONFLICT", + "DASHBOARD_ALREADY_MANAGED", + "LIBRARY_ELEMENT_NAME_CONFLICT", + "UNSUPPORTED_DATA_TYPE", + "RESOURCE_CONFLICT", + "UNEXPECTED_STATUS_CODE", + "INTERNAL_SERVICE_ERROR", + "ONLY_CORE_DATA_SOURCES", + "GENERIC_ERROR" + ], + "type": "string" + }, "message": { "type": "string" },