diff --git a/pkg/rancher-prime/l10n/en-us.yaml b/pkg/rancher-prime/l10n/en-us.yaml
index 649e3cdad0b..b9627ac4588 100644
--- a/pkg/rancher-prime/l10n/en-us.yaml
+++ b/pkg/rancher-prime/l10n/en-us.yaml
@@ -55,6 +55,11 @@ registration:
invalid: Invalid
expired: Expired
valid: Active
+ errors:
+ missing-code: Registration code as Secret is missing
+ mismatch-code: Registration active but does not match Secret registration code. Please verify the registration
+ generic-registration: No registration found for existing registration code (Secret).
+ timeout-registration: Timeout reached while waiting for resource.
prime:
installed: This is a SUSE Rancher Prime installation
diff --git a/pkg/rancher-prime/pages/registration.composable.test.ts b/pkg/rancher-prime/pages/registration.composable.test.ts
index 00f0f347221..6fc26afc7fb 100644
--- a/pkg/rancher-prime/pages/registration.composable.test.ts
+++ b/pkg/rancher-prime/pages/registration.composable.test.ts
@@ -19,7 +19,7 @@ describe('registration composable', () => {
describe('when initialized', () => {
it('should retrieve the registration secret and current registration', async() => {
- const value = 'whatever';
+ const regCode = 'whatever';
const hash = 'anything';
const secrets = [
{ metadata: { namespace: 'not me' } },
@@ -30,7 +30,7 @@ describe('registration composable', () => {
name: REGISTRATION_SECRET,
labels: { [REGISTRATION_LABEL]: hash }
},
- data: { regCode: btoa(value) }
+ data: { regCode: btoa(regCode) }
},
];
const registrations = [{
@@ -54,14 +54,14 @@ describe('registration composable', () => {
await initRegistration();
jest.setTimeout(0);
- expect(registrationCode.value).toStrictEqual(value);
+ expect(registrationCode.value).toStrictEqual(regCode);
expect(registration.value.active).toStrictEqual(true);
expect(registration.value.resourceLink).toStrictEqual('123');
expect(registrationStatus.value).toStrictEqual('registered');
});
it('should display the correct error message, prioritizing conditions without Failure', async() => {
- const value = 'whatever';
+ const regCode = 'whatever';
const hash = 'anything';
const errorMessage = 'Registration failed';
const secrets = [
@@ -73,7 +73,7 @@ describe('registration composable', () => {
name: REGISTRATION_SECRET,
labels: { [REGISTRATION_LABEL]: hash }
},
- data: { regCode: btoa(value) }
+ data: { regCode: btoa(regCode) }
},
];
const registrations = [{
@@ -315,7 +315,7 @@ describe('registration composable', () => {
});
describe('deregistering', () => {
- it('should reset all de values', async() => {
+ it('should reset all the values', async() => {
const {
registerOffline,
registrationStatus
@@ -326,4 +326,63 @@ describe('registration composable', () => {
expect(registrationStatus.value).toStrictEqual('registering-offline');
});
});
+
+ describe('should display an error message', () => {
+ it('with generic error if Registration Code is present but not active Registration is found', async() => {
+ const expectation = 'registration.errors.generic-registration';
+ const regCode = 'whatever';
+ const hash = 'anything';
+ const secrets = [{
+ metadata: {
+ namespace: REGISTRATION_NAMESPACE,
+ name: REGISTRATION_SECRET,
+ labels: { [REGISTRATION_LABEL]: hash }
+ },
+ data: { regCode: btoa(regCode) }
+ },
+ ];
+ const registrations = [] as any[];
+
+ dispatchSpy = jest.fn()
+ .mockReturnValueOnce(Promise.resolve(secrets))
+ .mockReturnValue(Promise.resolve(registrations));
+ const store = { state: {}, dispatch: dispatchSpy } as any;
+ const { initRegistration, errors } = usePrimeRegistration(store);
+
+ await initRegistration();
+ jest.setTimeout(0);
+
+ expect(errors.value[0]).toStrictEqual(expectation);
+ });
+
+ describe('registering online', () => {
+ it.skip('given no registration code', async() => {
+ const expectation = 'registration.errors.missing-code';
+ const store = { state: {}, dispatch: dispatchSpy } as any;
+ const { errors } = usePrimeRegistration(store);
+
+ expect(errors.value[0]).toStrictEqual(expectation);
+ });
+
+ it.skip('given a mismatched registration code', async() => {
+ const expectation = 'registration.errors.mismatch-code';
+ const store = { state: {}, dispatch: dispatchSpy } as any;
+ const { errors } = usePrimeRegistration(store);
+
+ expect(errors.value[0]).toStrictEqual(expectation);
+ });
+
+ it.skip('given no response', async() => {
+ const expectation = 'registration.errors.timeout-registration';
+ const store = { state: {}, dispatch: dispatchSpy } as any;
+ const { errors, registerOnline, registrationCode } = usePrimeRegistration(store);
+
+ registrationCode.value = 'not a real code';
+
+ await registerOnline((val: boolean) => true);
+
+ expect(errors.value[0]).toStrictEqual(expectation);
+ });
+ });
+ });
});
diff --git a/pkg/rancher-prime/pages/registration.composable.ts b/pkg/rancher-prime/pages/registration.composable.ts
index 19fbd873ac1..e5501eb1e45 100644
--- a/pkg/rancher-prime/pages/registration.composable.ts
+++ b/pkg/rancher-prime/pages/registration.composable.ts
@@ -8,6 +8,7 @@ import {
} from '../config/constants';
import { SECRET } from '@shell/config/types';
import { dateTimeFormat } from '@shell/utils/time';
+import { useI18n } from '@shell/composables/useI18n';
type RegistrationStatus = 'loading' | 'registering-online' | 'registration-request' | 'registering-offline' | 'registered' | null;
type AsyncButtonFunction = (val: boolean) => void;
@@ -21,6 +22,7 @@ interface RegistrationDashboard {
color: 'error' | 'success';
message: string;
status: 'valid' | 'error' | 'none';
+ code: string | null;
registrationLink?: string; // not generated on failure or reset
resourceLink?: string; // not generated on empty registration
}
@@ -89,6 +91,7 @@ const emptyRegistration: RegistrationDashboard = {
mode: '--',
expiration: '--',
color: 'error',
+ code: null,
message: 'registration.list.table.badge.none',
status: 'none'
};
@@ -110,6 +113,7 @@ const registrationBannerCases = {
export const usePrimeRegistration = (storeArg?: Store) => {
const store = storeArg ?? useStore();
+ const { t } = useI18n(store);
/**
* Registration mapped value used in the UI
@@ -338,12 +342,12 @@ export const usePrimeRegistration = (storeArg?: Store) => {
*/
const findRegistration = async(hash: string | null): Promise => {
const registrations: PartialRegistration[] = await store.dispatch('management/findAll', { type: REGISTRATION_RESOURCE_NAME }).catch(() => []) || [];
- const registration = registrations.find((registration) => registration.metadata?.labels[REGISTRATION_LABEL] === hash &&
+ const newRegistration = registrations.find((registration) => registration.metadata?.labels[REGISTRATION_LABEL] === hash &&
!isRegistrationOfflineProgress(registration) &&
isRegistrationCompleted(registration)
);
- return registration;
+ return newRegistration;
};
/**
@@ -381,6 +385,7 @@ export const usePrimeRegistration = (storeArg?: Store) => {
mode: registration.spec.mode,
registrationLink: registration.status?.activationStatus?.systemUrl,
resourceLink: registration.links.view,
+ code: registration?.metadata?.labels[REGISTRATION_LABEL],
};
if (isActive) {
@@ -399,7 +404,7 @@ export const usePrimeRegistration = (storeArg?: Store) => {
if (errorMessage) {
onError(errorMessage);
} else {
- onError(new Error('Registration failed without a specific error message'));
+ onError(new Error(t('registration.errors.generic-registration')));
}
return {
@@ -480,6 +485,32 @@ export const usePrimeRegistration = (storeArg?: Store) => {
await secret.save();
};
+ /**
+ * Generic fallback in case of unhandled errors based on existing resources
+ * @param polling
+ * @returns
+ */
+ const getError = (polling?: boolean): string => {
+ if (polling && !secret.value?.data?.regCode) {
+ return t('registration.errors.missing-code');
+ }
+
+ // Fallback in case of logic changes
+ if (polling && registration.value.active && secret.value?.data?.regCode !== registration.value?.code) {
+ return t('registration.errors.mismatch-code');
+ }
+
+ if (secret.value && !registration.value.active) {
+ return t('registration.errors.generic-registration');
+ }
+
+ if (polling) {
+ return t('registration.errors.timeout-registration');
+ }
+
+ return '';
+ };
+
/**
* Polls periodically until a condition is met or timeout is reached.
* @param fetchFn Function to fetch the resource (e.g., findRegistration or findOfflineRequest)
@@ -495,7 +526,7 @@ export const usePrimeRegistration = (storeArg?: Store) => {
mapResult: (resource: any) => T,
extraConditionFn?: (resource: any) => boolean,
frequency = 250,
- timeout = 10000
+ timeout = 10000 // First initialization is slow, which is most of the cases
): Promise => {
return new Promise((resolve, reject) => {
const startTime = Date.now();
@@ -503,7 +534,7 @@ export const usePrimeRegistration = (storeArg?: Store) => {
const interval = setInterval(async() => {
if (Date.now() - startTime > timeout) {
clearInterval(interval);
- reject(new Error('Timeout reached while waiting for resource'));
+ reject(new Error(getError(true)));
return;
}
@@ -532,6 +563,11 @@ export const usePrimeRegistration = (storeArg?: Store) => {
secret.value = await getSecret();
registrationCode.value = secret.value?.data?.regCode ? atob(secret.value.data.regCode) : null; // Get registration code from secret
registrationStatus.value = await getRegistration();
+ const message = getError();
+
+ if (message) {
+ onError(new Error(message));
+ }
};
return {