From e18ba4551bdb718d274c583e39a73c00bcca101b Mon Sep 17 00:00:00 2001 From: pablomendezroyo <41727368+pablomendezroyo@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:13:17 +0100 Subject: [PATCH] ensure requirements in `packageInstall` and not only on `fetchDnpRequest` (#2054) * ensure requirements in `packageInstall` and not only on `fetchDnpRequest` * increase test timeout * add check type dncore * skip bypass for testing * fix linter * improve error message * remove bypass install --- .../installer/src/calls/packageInstall.ts | 4 +- .../src/installer/checkInstallRequirements.ts | 68 +++++++++++++++++++ packages/installer/src/installer/index.ts | 1 + .../schemas/test/unit/validateSchema.test.ts | 3 +- 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 packages/installer/src/installer/checkInstallRequirements.ts diff --git a/packages/installer/src/calls/packageInstall.ts b/packages/installer/src/calls/packageInstall.ts index bd99fe9f7..944fe4fcb 100644 --- a/packages/installer/src/calls/packageInstall.ts +++ b/packages/installer/src/calls/packageInstall.ts @@ -10,7 +10,8 @@ import { rollbackPackages, writeAndValidateFiles, postInstallClean, - afterInstall + afterInstall, + checkInstallRequirements } from "../installer/index.js"; import { logs, getLogUi, logUiClear } from "@dappnode/logger"; import { Routes } from "@dappnode/types"; @@ -53,6 +54,7 @@ export async function packageInstall( if (!release.signedSafe && !options.BYPASS_SIGNED_RESTRICTION) { throw Error(`Package ${release.dnpName} is from untrusted origin and is not signed`); } + if (!release.isCore) await checkInstallRequirements({ manifest: release.manifest }); } // Gather all data necessary for the install diff --git a/packages/installer/src/installer/checkInstallRequirements.ts b/packages/installer/src/installer/checkInstallRequirements.ts new file mode 100644 index 000000000..621826d00 --- /dev/null +++ b/packages/installer/src/installer/checkInstallRequirements.ts @@ -0,0 +1,68 @@ +import { listPackages, getDockerVersion } from "@dappnode/dockerapi"; +import { params } from "@dappnode/params"; +import { Manifest, InstalledPackageData } from "@dappnode/types"; +import { valid, gt } from "semver"; + +/** + * Get the install requirements and throw an error if they are not met + */ +export async function checkInstallRequirements({ manifest }: { manifest: Manifest }): Promise { + if (manifest.type === "dncore") return; + const installedPackages = await listPackages(); + const packagesRequiredToBeUninstalled = getRequiresUninstallPackages({ manifest, installedPackages }); + const requiresCoreUpdate = getRequiresCoreUpdateTo({ manifest, installedPackages }); + const requiresDockerUpdate = await getRequiresDockerUpdateTo({ manifest }); + + const errors: string[] = []; + if (packagesRequiredToBeUninstalled.length > 0) + errors.push(`The following packages must be uninstalled: ${packagesRequiredToBeUninstalled.join(", ")}`); + if (requiresCoreUpdate) errors.push(`Core update required to ${requiresCoreUpdate}`); + if (requiresDockerUpdate) errors.push(`Docker update required to ${requiresDockerUpdate}`); + if (errors.length > 0) + throw new Error(`The package cannot be installed because of the following requirements: +${errors.join("\n")}`); +} + +function getRequiresUninstallPackages({ + manifest, + installedPackages +}: { + manifest: Manifest; + installedPackages: InstalledPackageData[]; +}): string[] { + const { notInstalledPackages } = manifest.requirements || {}; + if (!notInstalledPackages || notInstalledPackages.length === 0) return []; + return notInstalledPackages.filter((dnpName) => installedPackages.find((dnp) => dnp.dnpName === dnpName)); +} + +function getRequiresCoreUpdateTo({ + manifest, + installedPackages +}: { + manifest: Manifest; + installedPackages: InstalledPackageData[]; +}): string | null { + const coreVersion = installedPackages.find((dnp) => dnp.dnpName === params.coreDnpName)?.version; + const minDnVersion = manifest.requirements?.minimumDappnodeVersion; + + if (!coreVersion || !minDnVersion) return null; + + const requiresCoreUpdate = Boolean(valid(minDnVersion) && valid(coreVersion) && gt(minDnVersion, coreVersion)); + if (requiresCoreUpdate) return minDnVersion; + + return null; +} +async function getRequiresDockerUpdateTo({ manifest }: { manifest: Manifest }): Promise { + const minDockerVersion = manifest.requirements?.minimumDockerVersion; + if (!minDockerVersion) return null; + const currentDockerVersion = await getDockerVersion(); + const requiresDockerUpdate = Boolean( + minDockerVersion && + valid(minDockerVersion) && + valid(currentDockerVersion) && + gt(minDockerVersion, currentDockerVersion) + ); + + if (requiresDockerUpdate) return minDockerVersion; + return null; +} diff --git a/packages/installer/src/installer/index.ts b/packages/installer/src/installer/index.ts index 52727c69f..7f31a81de 100644 --- a/packages/installer/src/installer/index.ts +++ b/packages/installer/src/installer/index.ts @@ -7,3 +7,4 @@ export * from "./rollbackPackages.js"; export * from "./runPackages.js"; export * from "./restartPatch.js"; export * from "./writeAndValidateFiles.js"; +export * from "./checkInstallRequirements.js"; diff --git a/packages/schemas/test/unit/validateSchema.test.ts b/packages/schemas/test/unit/validateSchema.test.ts index b631fd2df..1a8478860 100644 --- a/packages/schemas/test/unit/validateSchema.test.ts +++ b/packages/schemas/test/unit/validateSchema.test.ts @@ -5,7 +5,8 @@ import path from "path"; import { cleanTestDir, testDir } from "../testUtils.js"; import { Manifest, SetupWizard } from "@dappnode/types"; -describe("schemaValidation", () => { +describe("schemaValidation", function () { + this.timeout(10000); describe("manifest", () => { before(() => { cleanTestDir();