diff --git a/packages/dappmanager/src/modules/https-portal/index.ts b/packages/dappmanager/src/modules/https-portal/index.ts index 6daacbad5..69404edb0 100644 --- a/packages/dappmanager/src/modules/https-portal/index.ts +++ b/packages/dappmanager/src/modules/https-portal/index.ts @@ -69,14 +69,18 @@ export class HttpsPortal { // Edit compose to persist the setting addNetworkAliasCompose(container, externalNetworkName, aliases); - // Check whether DNP_HTTPS compose has external network persisted - const httpsComposePath = ComposeEditor.getComposePath(params.HTTPS_PORTAL_DNPNAME, true) - const editor = new ComposeEditor(ComposeEditor.readFrom(httpsComposePath)) + const httpsComposePath = ComposeEditor.getComposePath( + params.HTTPS_PORTAL_DNPNAME, + true + ); + const editor = new ComposeEditor(ComposeEditor.readFrom(httpsComposePath)); - if(editor.getComposeNetwork(externalNetworkName) === null) { - const httpsExternalAlias = getExternalNetworkAlias(httpsPortalContainer) - addNetworkAliasCompose(httpsPortalContainer, externalNetworkName, [httpsExternalAlias]) + if (editor.getComposeNetwork(externalNetworkName) === null) { + const httpsExternalAlias = getExternalNetworkAlias(httpsPortalContainer); + addNetworkAliasCompose(httpsPortalContainer, externalNetworkName, [ + httpsExternalAlias + ]); } } @@ -146,6 +150,20 @@ export class HttpsPortal { return mappings; } + /** + * Returns true if the container has assigned a mapping to the https-portal + */ + async hasMapping(dnpName: string, serviceName: string): Promise { + const entries = await this.httpsPortalApiClient.list(); + const mappingAlias = getExternalNetworkAlias({ serviceName, dnpName }); + for (const { toHost } of entries) { + // toHost format: someDomain:80 + const alias = toHost.split(":")[0]; + if (alias === mappingAlias) return true; + } + return false; + } + private async getContainerForMapping( mapping: HttpsPortalMapping, containers?: PackageContainer[] diff --git a/packages/dappmanager/src/modules/installer/https.ts b/packages/dappmanager/src/modules/installer/https.ts new file mode 100644 index 000000000..814ad7635 --- /dev/null +++ b/packages/dappmanager/src/modules/installer/https.ts @@ -0,0 +1,149 @@ +import { listPackageNoThrow } from "../docker/list/listPackages"; +import { httpsPortal } from "../../calls/httpsPortal"; +import { prettyDnpName } from "../../utils/format"; +import params from "../../params"; +import { InstallPackageData } from "../../types"; +import { Log } from "../../utils/logUi"; +import { HttpsPortalMapping } from "../../common"; +import { getExternalNetworkAlias } from "../../domains"; +import { + dockerListNetworks, + dockerCreateNetwork, + dockerNetworkConnect +} from "../docker"; + +/** + * Connect to dnpublic_network with an alias if: + * - is HTTPS package + * - any package with https portal mappings + */ +export async function connectToPublicNetwork( + pkg: InstallPackageData, + externalNetworkName: string +): Promise { + // if there is no https, checks aren't needed + if (!(await isRunningHttps())) return; + + // create network if necessary + const networks = await dockerListNetworks(); + if (!networks.find(network => network.Name === externalNetworkName)) + await dockerCreateNetwork(externalNetworkName); + + const containers = + ( + await listPackageNoThrow({ + dnpName: pkg.dnpName + }) + )?.containers || []; + + if (containers.length === 0) return; + + for (const container of containers) { + if ( + pkg.dnpName === params.HTTPS_PORTAL_DNPNAME || + (await httpsPortal.hasMapping(pkg.dnpName, container.serviceName)) + ) { + const alias = getExternalNetworkAlias({ + serviceName: container.serviceName, + dnpName: pkg.dnpName + }); + + if (!container.networks.find(n => n.name === externalNetworkName)) { + await dockerNetworkConnect( + externalNetworkName, + container.containerName, + { Aliases: [alias] } + ); + } + } + } +} + +/** + * Expose default HTTPS ports on installation defined in the manifest - exposable + */ +export async function exposeByDefaultHttpsPorts( + pkg: InstallPackageData, + log: Log +): Promise { + if (pkg.metadata.exposable) { + // Requires that https package exists and it is running + if (!(await isRunningHttps())) + throw Error( + `HTTPS package not running but required to expose HTTPS ports by default.` + ); + + const currentMappings = await httpsPortal.getMappings(); + const portMappinRollback: HttpsPortalMapping[] = []; + + for (const exposable of pkg.metadata.exposable) { + if (exposable.exposeByDefault) { + const portalMapping: HttpsPortalMapping = { + fromSubdomain: exposable.fromSubdomain || prettyDnpName(pkg.dnpName), // get dnpName by default + dnpName: pkg.dnpName, + serviceName: + exposable.serviceName || Object.keys(pkg.compose.services)[0], // get first service name by default (docs: https://docs.dappnode.io/es/developers/manifest-reference/#servicename) + port: exposable.port + }; + + if ( + currentMappings.length > 0 && + currentMappings.includes(portalMapping) + ) + continue; + + try { + // Expose default HTTPS ports + log( + pkg.dnpName, + `Exposing ${prettyDnpName(pkg.dnpName)}:${ + exposable.port + } to the external internet` + ); + await httpsPortal.addMapping(portalMapping); + portMappinRollback.push(portalMapping); + + log( + pkg.dnpName, + `Exposed ${prettyDnpName(pkg.dnpName)}:${ + exposable.port + } to the external internet` + ); + } catch (e) { + e.message = `${e.message} Error exposing default HTTPS ports, removing mappings`; + for (const mappingRollback of portMappinRollback) { + await httpsPortal.removeMapping(mappingRollback).catch(e => { + log( + pkg.dnpName, + `Error removing mapping ${JSON.stringify(mappingRollback)}, ${ + e.message + }` + ); + }); + } + throw e; + } + } + } + } +} + +// Utils + +/** + * Returns true if HTTPS package installed and running, otherwise return false + */ +async function isRunningHttps() { + const httpsPackage = await listPackageNoThrow({ + dnpName: params.HTTPS_PORTAL_DNPNAME + }); + + if (!httpsPackage) return false; + + // Check every HTTPS container is running + httpsPackage.containers.forEach(container => { + if (!container.running) return false; + }); + + return true; +} diff --git a/packages/dappmanager/src/modules/installer/runPackages.ts b/packages/dappmanager/src/modules/installer/runPackages.ts index 61f1eaf15..fe17985c2 100644 --- a/packages/dappmanager/src/modules/installer/runPackages.ts +++ b/packages/dappmanager/src/modules/installer/runPackages.ts @@ -6,10 +6,11 @@ import { Log } from "../../utils/logUi"; import { copyFileTo } from "../../calls/copyFileTo"; import { InstallPackageData } from "../../types"; import { logs } from "../../logs"; -import { dockerComposeUpPackage, dockerCreateNetwork, dockerListNetworks } from "../docker"; +import { dockerComposeUpPackage } from "../docker"; import { packageToInstallHasPid } from "../../utils/pid"; -import { exposeByDefaultHttpsPorts } from "./exposeByDefaultHttpsPorts"; -import { ComposeFileEditor } from "../compose/editor"; +import { connectToPublicNetwork, exposeByDefaultHttpsPorts } from "./https"; + +const externalNetworkName = params.DNP_EXTERNAL_NETWORK_NAME; /** * Create and run each package container in series @@ -40,29 +41,6 @@ export async function runPackages( // - Allow copying files without duplicating logic // - Allow conditionally starting containers latter if were previously running log(pkg.dnpName, "Preparing package..."); - - - // Recreate HTTPs portal mapping if installing or updating HTTPs package - if (pkg.dnpName === params.HTTPS_PORTAL_DNPNAME) { - log(pkg.dnpName, "Ensuring HTTPS network exists..."); - - const networks = await dockerListNetworks(); - const externalNetworkName = params.DNP_EXTERNAL_NETWORK_NAME; - if (!networks.find(network => network.Name === externalNetworkName)) { - await dockerCreateNetwork(externalNetworkName); - } - - // Check whether DNP_HTTPS compose has external network persisted - const compose = new ComposeFileEditor(pkg.dnpName, true); - if(compose.getComposeNetwork(externalNetworkName) === null) { - log(pkg.dnpName, "Adding external network to HTTPS compose..."); - const composeService = compose.services()["https.dnp.dappnode.eth"]; - const aliases: string[] = ["https.external"]; - composeService.addNetwork(externalNetworkName, {aliases}); - compose.write(); - } - } - await dockerComposeUp(pkg.composePath, { // To clean-up changing multi-service packages, remove orphans // but NOT for core packages, which always have orphans @@ -104,6 +82,8 @@ export async function runPackages( log(pkg.dnpName, "Package started"); // Expose default HTTPs ports if required + + await connectToPublicNetwork(pkg, externalNetworkName); await exposeByDefaultHttpsPorts(pkg, log); } }