Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/rancher-prime/l10n/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="${ url }">registration</a>
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
Expand Down
71 changes: 65 additions & 6 deletions pkg/rancher-prime/pages/registration.composable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' } },
Expand All @@ -30,7 +30,7 @@ describe('registration composable', () => {
name: REGISTRATION_SECRET,
labels: { [REGISTRATION_LABEL]: hash }
},
data: { regCode: btoa(value) }
data: { regCode: btoa(regCode) }
},
];
const registrations = [{
Expand All @@ -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 = [
Expand All @@ -73,7 +73,7 @@ describe('registration composable', () => {
name: REGISTRATION_SECRET,
labels: { [REGISTRATION_LABEL]: hash }
},
data: { regCode: btoa(value) }
data: { regCode: btoa(regCode) }
},
];
const registrations = [{
Expand Down Expand Up @@ -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
Expand All @@ -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);
});
});
});
});
46 changes: 41 additions & 5 deletions pkg/rancher-prime/pages/registration.composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
}
Expand Down Expand Up @@ -89,6 +91,7 @@ const emptyRegistration: RegistrationDashboard = {
mode: '--',
expiration: '--',
color: 'error',
code: null,
message: 'registration.list.table.badge.none',
status: 'none'
};
Expand All @@ -110,6 +113,7 @@ const registrationBannerCases = {

export const usePrimeRegistration = (storeArg?: Store<any>) => {
const store = storeArg ?? useStore();
const { t } = useI18n(store);

/**
* Registration mapped value used in the UI
Expand Down Expand Up @@ -338,12 +342,12 @@ export const usePrimeRegistration = (storeArg?: Store<any>) => {
*/
const findRegistration = async(hash: string | null): Promise<PartialRegistration | undefined> => {
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;
};

/**
Expand Down Expand Up @@ -381,6 +385,7 @@ export const usePrimeRegistration = (storeArg?: Store<any>) => {
mode: registration.spec.mode,
registrationLink: registration.status?.activationStatus?.systemUrl,
resourceLink: registration.links.view,
code: registration?.metadata?.labels[REGISTRATION_LABEL],
};

if (isActive) {
Expand All @@ -399,7 +404,7 @@ export const usePrimeRegistration = (storeArg?: Store<any>) => {
if (errorMessage) {
onError(errorMessage);
} else {
onError(new Error('Registration failed without a specific error message'));
onError(new Error(t('registration.errors.generic-registration')));
}

return {
Expand Down Expand Up @@ -480,6 +485,32 @@ export const usePrimeRegistration = (storeArg?: Store<any>) => {
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)
Expand All @@ -495,15 +526,15 @@ export const usePrimeRegistration = (storeArg?: Store<any>) => {
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<T> => {
return new Promise<T>((resolve, reject) => {
const startTime = Date.now();

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;
}
Expand Down Expand Up @@ -532,6 +563,11 @@ export const usePrimeRegistration = (storeArg?: Store<any>) => {
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 {
Expand Down