diff --git a/controllers/license/api/v1/license_types.go b/controllers/license/api/v1/license_types.go index 696e329294b6..79a97bc65a47 100644 --- a/controllers/license/api/v1/license_types.go +++ b/controllers/license/api/v1/license_types.go @@ -58,6 +58,7 @@ type LicenseStatus struct { //+kubebuilder:validation:Enum=Pending;Failed;Active;Expired;Invalid;Mismatch //+kubebuilder:default=Pending Phase LicenseStatusPhase `json:"phase,omitempty"` + Code ValidationCode `json:"code,omitempty"` Reason string `json:"reason,omitempty"` ActivationTime metav1.Time `json:"activationTime,omitempty"` ExpirationTime metav1.Time `json:"expirationTime,omitempty"` diff --git a/controllers/license/config/crd/bases/license.sealos.io_licenses.yaml b/controllers/license/config/crd/bases/license.sealos.io_licenses.yaml index 676704216cc6..94484c1d72c1 100644 --- a/controllers/license/config/crd/bases/license.sealos.io_licenses.yaml +++ b/controllers/license/config/crd/bases/license.sealos.io_licenses.yaml @@ -57,6 +57,8 @@ spec: activationTime: format: date-time type: string + code: + type: integer expirationTime: format: date-time type: string diff --git a/controllers/license/deploy/charts/license-controller/crds/license.sealos.io_licenses.yaml b/controllers/license/deploy/charts/license-controller/crds/license.sealos.io_licenses.yaml index 676704216cc6..94484c1d72c1 100644 --- a/controllers/license/deploy/charts/license-controller/crds/license.sealos.io_licenses.yaml +++ b/controllers/license/deploy/charts/license-controller/crds/license.sealos.io_licenses.yaml @@ -57,6 +57,8 @@ spec: activationTime: format: date-time type: string + code: + type: integer expirationTime: format: date-time type: string diff --git a/controllers/license/internal/controller/license_controller.go b/controllers/license/internal/controller/license_controller.go index 6b4cd6da5aa9..a83fcd529302 100644 --- a/controllers/license/internal/controller/license_controller.go +++ b/controllers/license/internal/controller/license_controller.go @@ -139,9 +139,11 @@ func (r *LicenseReconciler) reconcile( var reason string var validationErr licenseutil.ValidationError + validationCode := licensev1.ValidationError var phase licensev1.LicenseStatusPhase if errors.As(err, &validationErr) { reason = validationErr.Message + validationCode = validationErr.Code switch validationErr.Code { case licensev1.ValidationExpired: phase = licensev1.LicenseStatusPhaseExpired @@ -159,6 +161,7 @@ func (r *LicenseReconciler) reconcile( } updateStatus := &license.Status updateStatus.Phase = phase + updateStatus.Code = validationCode updateStatus.Reason = reason updateStatus.ActivationTime = license.Status.ActivationTime updateStatus.ExpirationTime = license.Status.ExpirationTime @@ -182,6 +185,7 @@ func (r *LicenseReconciler) reconcile( if err := r.activator.Active(ctx, license); err != nil { failStatus := &license.Status failStatus.Phase = licensev1.LicenseStatusPhaseFailed + failStatus.Code = licensev1.ValidationError failStatus.Reason = fmt.Sprintf("license activation failed: %v", err) failStatus.ActivationTime = license.Status.ActivationTime failStatus.ExpirationTime = license.Status.ExpirationTime diff --git a/frontend/providers/license/public/locales/en/common.json b/frontend/providers/license/public/locales/en/common.json index 76edaf98e91e..ba2003e227c5 100644 --- a/frontend/providers/license/public/locales/en/common.json +++ b/frontend/providers/license/public/locales/en/common.json @@ -32,5 +32,9 @@ "file.upload error description": "Failed to upload files", "Cancel": "Cancel", "file.Select a maximum of 10 files": "You can select no more than 10 files", - "Copy Failed": "Failed to copy" + "Copy Failed": "Failed to copy", + "LICENSE_VALIDATION_generic": "License validation failed. Check the license content and try again.", + "LICENSE_VALIDATION_expired": "The current license has expired. Renew it and try again.", + "LICENSE_VALIDATION_clusterIdMismatch": "This license does not match the current cluster. Verify the cluster ID and try again.", + "LICENSE_VALIDATION_clusterInfoMismatch": "The current cluster resources do not satisfy the license constraints. Check node, CPU, memory, and user limits." } diff --git a/frontend/providers/license/public/locales/zh/common.json b/frontend/providers/license/public/locales/zh/common.json index 1a8d29879675..f33f4f5de593 100644 --- a/frontend/providers/license/public/locales/zh/common.json +++ b/frontend/providers/license/public/locales/zh/common.json @@ -32,5 +32,9 @@ "file.upload error description": "文件上传失败", "Cancel": "取消", "file.Select a maximum of 10 files": "至多选择 10 个文件", - "Copy Failed": "复制失败" + "Copy Failed": "复制失败", + "LICENSE_VALIDATION_generic": "License 校验失败,请检查许可证内容后重试", + "LICENSE_VALIDATION_expired": "当前 License 已过期,请续期后重试", + "LICENSE_VALIDATION_clusterIdMismatch": "当前 License 与本集群不匹配,请确认集群 ID 后重试", + "LICENSE_VALIDATION_clusterInfoMismatch": "当前集群资源不满足 License 约束,请核对节点、CPU、内存和用户数限制" } diff --git a/frontend/providers/license/src/pages/api/applyYamlList.ts b/frontend/providers/license/src/pages/api/applyYamlList.ts index 43a64747853d..2023833e9a89 100644 --- a/frontend/providers/license/src/pages/api/applyYamlList.ts +++ b/frontend/providers/license/src/pages/api/applyYamlList.ts @@ -2,6 +2,7 @@ import { authSession } from '@/services/backend/auth'; import { getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; +import { getLicenseErrorCode } from '@/utils/licenseError'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -30,6 +31,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< console.log(err, '------'); jsonRes(res, { code: 500, + errorCode: + getLicenseErrorCode(err?.body?.details?.code) || getLicenseErrorCode(err?.body?.code), error: err }); } diff --git a/frontend/providers/license/src/pages/index.tsx b/frontend/providers/license/src/pages/index.tsx index 64586989ea98..1f79d0e86f24 100644 --- a/frontend/providers/license/src/pages/index.tsx +++ b/frontend/providers/license/src/pages/index.tsx @@ -29,6 +29,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { debounce } from 'lodash'; import yaml from 'js-yaml'; import { useTranslation } from 'next-i18next'; +import { getLicenseErrorMessage } from '@/utils/licenseError'; import { useEffect, useMemo, useState } from 'react'; export default function LicenseApp() { @@ -139,7 +140,10 @@ export default function LicenseApp() { }); } else if (licenseResult.status.phase !== 'Active') { toast({ - title: licenseResult.status.reason, + title: getLicenseErrorMessage(t, { + validationCode: licenseResult.status.code, + fallback: licenseResult.status.reason + }), status: 'error' }); } else { @@ -152,10 +156,13 @@ export default function LicenseApp() { } queryClient.invalidateQueries(['getLicenseActive']); }, - onError(error: { message?: string }) { + onError(error: { message?: string; errorCode?: string | number }) { if (error?.message && typeof error?.message === 'string') { toast({ - title: error.message, + title: getLicenseErrorMessage(t, { + errorCode: error.errorCode, + fallback: error.message + }), status: 'error' }); } diff --git a/frontend/providers/license/src/services/backend/response.ts b/frontend/providers/license/src/services/backend/response.ts index 0aa716c03358..ba3bca056092 100644 --- a/frontend/providers/license/src/services/backend/response.ts +++ b/frontend/providers/license/src/services/backend/response.ts @@ -6,11 +6,12 @@ export const jsonRes = ( props?: { code?: number; message?: string; + errorCode?: string | number; data?: T; error?: any; } ) => { - const { code = 200, message = '', data = null, error } = props || {}; + const { code = 200, message = '', errorCode, data = null, error } = props || {}; // Specified error if (typeof error === 'string' && ERROR_RESPONSE[error]) { @@ -33,6 +34,7 @@ export const jsonRes = ( res.json({ code, statusText: '', + errorCode, message: msg, data: data || error || null }); diff --git a/frontend/providers/license/src/services/kubernet.ts b/frontend/providers/license/src/services/kubernet.ts index 1990ffd9ecaf..ab783d3eb9a0 100644 --- a/frontend/providers/license/src/services/kubernet.ts +++ b/frontend/providers/license/src/services/kubernet.ts @@ -1,6 +1,7 @@ export interface ApiResp { code: number; message: string; + errorCode?: string | number; data?: T; } diff --git a/frontend/providers/license/src/types/license.d.ts b/frontend/providers/license/src/types/license.d.ts index dbd7e57f7376..72c1835f6684 100644 --- a/frontend/providers/license/src/types/license.d.ts +++ b/frontend/providers/license/src/types/license.d.ts @@ -60,6 +60,7 @@ export type LicenseCR = { type: string; }; status: { + code?: number; activationTime: string; expirationTime: string; phase: 'Active' | 'Failed'; diff --git a/frontend/providers/license/src/utils/licenseError.ts b/frontend/providers/license/src/utils/licenseError.ts new file mode 100644 index 000000000000..0472c5f5da93 --- /dev/null +++ b/frontend/providers/license/src/utils/licenseError.ts @@ -0,0 +1,38 @@ +const LICENSE_ERROR_CODE_PREFIX = 'LICENSE_VALIDATION_'; + +export const LICENSE_ERROR_KEYS: Record = { + 1: 'generic', + 2: 'expired', + 3: 'clusterIdMismatch', + 4: 'clusterInfoMismatch' +}; + +export const getLicenseErrorCode = (code?: number) => { + if (typeof code !== 'number') return undefined; + const key = LICENSE_ERROR_KEYS[code]; + if (!key) return undefined; + return `${LICENSE_ERROR_CODE_PREFIX}${key}`; +}; + +export const getLicenseErrorMessage = ( + t: (key: string) => string, + options: { + errorCode?: string | number; + validationCode?: number; + fallback?: string; + } +) => { + const keyFromErrorCode = + typeof options.errorCode === 'string' && options.errorCode.startsWith(LICENSE_ERROR_CODE_PREFIX) + ? options.errorCode + : undefined; + const keyFromValidationCode = getLicenseErrorCode(options.validationCode); + const translationKey = keyFromErrorCode || keyFromValidationCode; + + if (translationKey) { + const translated = t(translationKey); + if (translated !== translationKey) return translated; + } + + return options.fallback || t('LICENSE_VALIDATION_generic'); +};