UNPKG

@dappnode/schemas

Version:

A shared TypeScript JSON schemas and its validation functions for the manifest and setup wizard dappnode files

168 lines 8.48 kB
import semver from "semver"; import { dockerComposeSafeKeys, } from "@dappnode/types"; import { dockerParams } from "./params.js"; let aggregatedError; function err(msg) { aggregatedError.push(msg); } /** * Validates against custom dappnode docker compose specs. * This function must be executed after the official docker schema * @param param0 */ export function validateDappnodeCompose(compose, manifest) { // clean the errors aggregatedError = []; const isCore = manifest.type === "dncore"; // COMPOSE TOP LEVEL restrictions validateComposeVersion(compose); validateComposeNetworks(compose); // SERVICE LEVEL restrictions const servicesNames = Object.keys(compose.services); for (const serviceName of servicesNames) { validateComposeService(compose, isCore, serviceName, manifest.name); } if (aggregatedError.length > 0) throw Error(`Error validating compose file with dappnode requirements:\n\n${aggregatedError.join("\n")}`); } /** * Ensures the docker compose version is supported */ function validateComposeVersion(compose) { if (semver.lt(compose.version + ".0", dockerParams.MINIMUM_COMPOSE_FILE_VERSION + ".0")) err(`Compose version ${compose.version} is not supported. Minimum version is ${dockerParams.MINIMUM_COMPOSE_FILE_VERSION}`); } /** * Ensures the docker compose networks are whitelisted */ function validateComposeNetworks(compose) { const networks = compose.networks; if (networks) { for (const networkName of Object.keys(networks)) { // Check there are only defined whitelisted compose networks if (!dockerParams.DOCKER_WHITELIST_NETWORKS.includes(networkName)) err(`The docker network ${networkName} is not allowed. Only docker networks ${dockerParams.DOCKER_WHITELIST_NETWORKS.join(",")} are allowed`); // Check all networks are external const network = networks[networkName]; if (network && network.external === false) err(`The docker network ${networkName} is not allowed. Docker internal networks are not allowed`); } } } /** * Ensures the compose keys values are valid for dappnode */ function validateComposeService(compose, isCore, serviceName, dnpName) { for (const serviceKey of Object.keys(compose.services[serviceName])) { if (!dockerComposeSafeKeys.includes(serviceKey)) err(`service ${serviceName} has key ${serviceKey} that is not allowed. Allowed keys are: ${dockerComposeSafeKeys.join(",")}`); } const { dns, pid, privileged, network_mode, volumes } = compose.services[serviceName]; // Check that if defined, the DNS must be the one provided from the bind package if (!isCore && dns && !dockerParams.DNS_SERVICE.includes(dns)) err(`service ${serviceName} has DNS different than ${dockerParams.DNS_SERVICE}`); // Check compose pid feature can only be used with the format service:*. The pid:host is dangerous if (pid && !pid.startsWith("service:")) err(`service ${serviceName} has PID feature differnet than service:*`); // Check only core packages or exporter cand be privileged if (dnpName !== "dappnode-exporter.dnp.dappnode.eth" && !isCore && privileged && privileged === true) err(`service ${serviceName} has privileged as true but is not a core package`); // Check Only core packages can use network_mode: host if (!isCore && network_mode && network_mode === "host") err(`service ${serviceName} has network_mode: host but is not a core package`); validateComposeServiceNetworks(compose, isCore, serviceName); if (volumes && !dockerParams.DOCKER_WHITELIST_BIND_VOLUMES.includes(dnpName)) { for (const [i, volume] of volumes.entries()) { if (typeof volume !== "string") { // https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3 err(`service ${serviceName}.volumes[${i}] must use volume short-syntax`); } validateComposeServiceVolumes(compose, serviceName, volume); } } } /** * Ensure the services networks are whitelisted */ function validateComposeServiceNetworks(compose, isCore, serviceName) { const DOCKER_WHITELIST_NETWORKS_STR = dockerParams.DOCKER_WHITELIST_NETWORKS.join(","); const DOCKER_WHITELIST_ALIASES_STR = dockerParams.DOCKER_CORE_ALIASES.join(","); const service = compose.services[serviceName]; const serviceNetworks = service.networks; if (!serviceNetworks) return; if (Array.isArray(serviceNetworks)) { for (const serviceNetwork of serviceNetworks) { // Check docker network is whitelisted when defined in array format if (!dockerParams.DOCKER_WHITELIST_NETWORKS.includes(serviceNetwork)) err(`service ${serviceName} has a non-whitelisted docker network: ${serviceNetwork}. Only docker networks ${DOCKER_WHITELIST_NETWORKS_STR} are allowed`); } } else { for (const serviceNetworkObjectName of Object.keys(serviceNetworks)) { // Check docker network is whitelisted when defined in object format if (!dockerParams.DOCKER_WHITELIST_NETWORKS.includes(serviceNetworkObjectName)) err(`service ${serviceName} has a non-whitelisted docker network: ${serviceNetworkObjectName}. Only docker networks ${DOCKER_WHITELIST_NETWORKS_STR} are allowed`); // Check core aliases are not used by non core packages const { aliases } = serviceNetworks[serviceNetworkObjectName]; if (!isCore && aliases && dockerParams.DOCKER_CORE_ALIASES.some((coreAlias) => aliases.includes(coreAlias))) { err(`service ${serviceName} has the network ${serviceNetworkObjectName} with reserved docker alias. Aliases ${DOCKER_WHITELIST_ALIASES_STR} are reserved to core packages`); } } } } /** * Ensure only core packages can use bind-mounted volumes */ function validateComposeServiceVolumes(compose, serviceName, volume) { // From https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3 // docker supports multiple short-syntax. DAppNode only supports exclicit declaration of name // // # Just specify a path and let the Engine create a volume // - /var/lib/mysql <- NOK // # Specify an absolute path mapping // - /opt/data:/var/lib/mysql <- NOK // # Path on the host, relative to the Compose file // - ./cache:/tmp/cache <- NOK // # User-relative path // - ~/configs:/etc/configs/:ro <- NOK // # Named volume // - datavolume:/var/lib/mysql <- OK // [volumeName, targetPath, modes] const [volumeName, targetPath] = volume.split(":"); if (!volumeName || !targetPath) { return err(`service ${serviceName} volume ${volume} must use short-syntax declaring volume exclitly: - datavolume:/var/lib/mysql <- OK - ./cache:/tmp/cache <- NOK bind mounts are forbidden unless explicitly whitelisted. Reach out to DAppNode team for that. `); } // Extra check REDUNDANT but for better UX in case developers use bind-mounts if (volumeName.includes("/")) { return err(`service ${serviceName} volume ${volume} is a bind-mount, only named non-external volumes are allowed`); } // Check volume name contains only valid charaters. // Note: this validation is also done by Docker. // Note: this also protects against weird paths. if (!/^[a-zA-Z0-9_.-]+$/.test(volumeName)) { return err(`service ${serviceName} volume ${volume} must only include characters [a-zA-Z0-9_.-]`); } // Check that service volumes are defined also at top compose level const volumeDefinition = compose.volumes?.[volumeName]; if (!volumeDefinition) return err(`service ${serviceName} volume ${volumeName} must have a volume definition at the top-level volumes section`); // Extra check REDUNDANT but for better UX if (volumeDefinition?.external) { err(`service ${serviceName} volume ${volumeName} is external. Only named non-external volumes are allowed`); } if (Object.keys(volumeDefinition).length > 0) { err(`service ${serviceName} volume ${volumeName} definition must not set any property`); } } //# sourceMappingURL=validateDappnodeCompose.js.map