diff --git a/src/help/snapshot.upload.help.ts b/src/help/snapshot.upload.help.ts index 07d0223c..1e34dbc0 100644 --- a/src/help/snapshot.upload.help.ts +++ b/src/help/snapshot.upload.help.ts @@ -2,13 +2,20 @@ import {cyan, green, magenta, yellow} from 'kleur'; import {OPTION_HELP, OPTIONS_ENV, SNAPSHOT_UPLOAD_DESCRIPTION} from '../constants/help.constants'; import {helpOutput} from './common.help'; import {TITLE} from './help'; +import {TARGET_OPTION_NOTE, targetOption} from './target.help'; const usage = `Usage: ${green('juno')} ${cyan('snapshot')} ${magenta('upload')} ${yellow('[options]')} Options: ${yellow('--dir')} Path to the snapshot directory that contains the metadata.json and chunks. + ${targetOption('snapshot')} + ${yellow('--target-id')} The module ID of a specific target to upload the snapshot to. ${OPTIONS_ENV} - ${OPTION_HELP}`; + ${OPTION_HELP} + +Notes: + +${TARGET_OPTION_NOTE}`; const doc = `${SNAPSHOT_UPLOAD_DESCRIPTION} diff --git a/src/services/modules/snapshot/snapshot.services.ts b/src/services/modules/snapshot/snapshot.services.ts index e7409184..2116d69c 100644 --- a/src/services/modules/snapshot/snapshot.services.ts +++ b/src/services/modules/snapshot/snapshot.services.ts @@ -1,8 +1,16 @@ import type {snapshot_id} from '@dfinity/ic-management'; import {encodeSnapshotId} from '@dfinity/ic-management'; import {Principal} from '@dfinity/principal'; +import {nonNullish, notEmptyString} from '@dfinity/utils'; +import {findJunoPackageDependency, getJunoPackage} from '@junobuild/admin'; import {nextArg} from '@junobuild/cli-tools'; +import { + JUNO_PACKAGE_MISSION_CONTROL_ID, + JUNO_PACKAGE_ORBITER_ID, + JUNO_PACKAGE_SATELLITE_ID +} from '@junobuild/config'; import ora from 'ora'; +import {actorParameters} from '../../../api/actor.api'; import { deleteCanisterSnapshot, loadCanisterSnapshot, @@ -126,11 +134,55 @@ export const uploadSnapshot = async ({ segment: AssetKey; args?: string[]; }) => { - const canisterId = Principal.fromText(cId); - const folder = nextArg({args, option: '--dir'}); assertNonNullishFolderExists(folder); + const targetCanisterId = nextArg({args, option: '--target-id'}); + + if (notEmptyString(targetCanisterId)) { + // TODO: extract into a service we can reuse for upgrade as well + const actorParams = await actorParameters(); + + const pkg = await getJunoPackage({ + moduleId: targetCanisterId, + ...actorParams + }); + + const validJunoPackage = (): {valid: boolean} => { + if (segment === 'mission_control') { + return {valid: pkg?.name === JUNO_PACKAGE_MISSION_CONTROL_ID}; + } + + if (segment === 'orbiter') { + return {valid: pkg?.name === JUNO_PACKAGE_ORBITER_ID}; + } + + // It's stock + if (pkg?.name === JUNO_PACKAGE_SATELLITE_ID) { + return {valid: true}; + } + + const {dependencies} = pkg ?? {dependencies: {}}; + + const satelliteDependency = findJunoPackageDependency({ + dependencies, + dependencyId: JUNO_PACKAGE_SATELLITE_ID + }); + + return {valid: nonNullish(satelliteDependency)}; + }; + + const {valid} = validJunoPackage(); + + if (!valid) { + await confirmAndExit( + `⚠️ The selected target is not a ${displaySegment(segment)}; this may cause issues if restored later. Are you sure you want to continue?` + ); + } + } + + const canisterId = Principal.fromText(notEmptyString(targetCanisterId) ? targetCanisterId : cId); + const {snapshotId} = await loadSnapshotAndAssertOverwrite({canisterId, segment}); await uploadExistingSnapshot({