diff --git a/.mochapodrc.yml b/.mochapodrc.yml index 64773b44..5c0ad6aa 100644 --- a/.mochapodrc.yml +++ b/.mochapodrc.yml @@ -6,16 +6,15 @@ testfs: # them in the local source. These can be overriden # in the `testfs` configuration. filesystem: - /mnt/root: - /mnt/boot: - config.json: - from: test/data/testconfig.json - config.txt: - from: test/data/mnt/boot/config.txt - device-type.json: - from: test/data/mnt/boot/device-type.json - /etc/os-release: + /mnt/boot: + os-release: from: test/data/etc/os-release + config.json: + from: test/data/testconfig.json + config.txt: + from: test/data/mnt/boot/config.txt + device-type.json: + from: test/data/mnt/boot/device-type.json # The `keep` list defines files that already exist in the # filesystem and need to be backed up before setting up the test environment keep: [] @@ -25,4 +24,4 @@ testfs: - /data/database.sqlite - /data/apps.json.preloaded - /mnt/root/tmp/balena-supervisor/**/*.lock - - /mnt/root/mnt/boot/splash/*.png + - /mnt/boot/splash/*.png diff --git a/Dockerfile.template b/Dockerfile.template index 5819b2ef..fed7f129 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -91,6 +91,9 @@ COPY --from=extra /usr/bin/lockfile /usr/bin/lockfile # Copy journalctl and library dependecies to the final image COPY --from=journal /sysroot / +# Copy mount script for mounting host partitions into container +COPY mount-partitions.sh . + # Runtime dependencies RUN apk add --no-cache \ ca-certificates \ @@ -100,12 +103,12 @@ RUN apk add --no-cache \ dbus \ libstdc++ \ dmidecode \ - sqlite-libs + sqlite-libs \ + lsblk ARG ARCH ARG VERSION=master -ENV CONFIG_MOUNT_POINT=/boot/config.json \ - LED_FILE=/dev/null \ +ENV LED_FILE=/dev/null \ SUPERVISOR_IMAGE=balena/$ARCH-supervisor \ VERSION=$VERSION diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 88356754..167487fe 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -18,10 +18,8 @@ services: environment: DOCKER_HOST: tcp://docker:2375 DBUS_SYSTEM_BUS_ADDRESS: unix:path=/run/dbus/system_bus_socket - # Required by migrations - CONFIG_MOUNT_POINT: /mnt/root/mnt/boot/config.json - # Read by constants to setup `bootMountpoint` - BOOT_MOUNTPOINT: /mnt/boot + # Required to skip device mounting in test env + TEST: 1 depends_on: - docker - dbus @@ -29,6 +27,7 @@ services: volumes: - dbus:/run/dbus - ./test/data/root:/mnt/root + - ./test/data/root/mnt/boot:/mnt/boot - ./test/lib/wait-for-it.sh:/wait-for-it.sh tmpfs: - /data # sqlite3 database @@ -61,6 +60,9 @@ services: environment: DOCKER_TLS_CERTDIR: '' command: --tls=false # --debug + tmpfs: + # Prevent dind creating a bunch of anonymous volumes on host + - /var/lib/docker sut: # Build the supervisor code for development and testing @@ -94,9 +96,11 @@ services: DBUS_SYSTEM_BUS_ADDRESS: unix:path=/run/dbus/system_bus_socket BALENA_SUPERVISOR_ADDRESS: http://balena-supervisor:48484 # Required by migrations - CONFIG_MOUNT_POINT: /mnt/root/mnt/boot/config.json - # Read by constants to setup `bootMountpoint` + CONFIG_MOUNT_POINT: /mnt/boot/config.json + # Required to set mountpoints normally set in entry.sh + ROOT_MOUNTPOINT: /mnt/root BOOT_MOUNTPOINT: /mnt/boot + HOST_OS_VERSION_PATH: /mnt/boot/os-release # Set required mounts as tmpfs or volumes here # if specific files need to be backed up between tests, # make sure to add them to the `testfs` configuration under @@ -104,6 +108,7 @@ services: tmpfs: - /data - /mnt/root + - /mnt/boot volumes: dbus: diff --git a/entry.sh b/entry.sh index 54fa2ae0..da682053 100755 --- a/entry.sh +++ b/entry.sh @@ -2,17 +2,20 @@ set -o errexit +# Mounts boot, state, & data partitions from balenaOS. +source ./mount-partitions.sh + # If the legacy /tmp/resin-supervisor exists on the host, a container might # already be using to take an update lock, so we symlink it to the new # location so that the supervisor can see it -[ -d /mnt/root/tmp/resin-supervisor ] && - ([ -d /mnt/root/tmp/balena-supervisor ] || ln -s ./resin-supervisor /mnt/root/tmp/balena-supervisor) +[ -d "${ROOT_MOUNTPOINT}"/tmp/resin-supervisor ] && + ([ -d "${ROOT_MOUNTPOINT}"/tmp/balena-supervisor ] || ln -s ./resin-supervisor "${ROOT_MOUNTPOINT}"/tmp/balena-supervisor) # Otherwise, if the lockfiles directory doesn't exist -[ -d /mnt/root/tmp/balena-supervisor ] || - mkdir -p /mnt/root/tmp/balena-supervisor +[ -d "${ROOT_MOUNTPOINT}"/tmp/balena-supervisor ] || + mkdir -p "${ROOT_MOUNTPOINT}"/tmp/balena-supervisor -export DBUS_SYSTEM_BUS_ADDRESS="${DBUS_SYSTEM_BUS_ADDRESS:-unix:path=/mnt/root/run/dbus/system_bus_socket}" +export DBUS_SYSTEM_BUS_ADDRESS="${DBUS_SYSTEM_BUS_ADDRESS:-unix:path="${ROOT_MOUNTPOINT}"/run/dbus/system_bus_socket}" # Include self-signed CAs, should they exist if [ -n "${BALENA_ROOT_CA}" ]; then @@ -30,7 +33,7 @@ fi # NOTE: this won't be necessary once the supervisor can update # itself, as using the label io.balena.features.journal-logs will # achieve the same objective -if { [ ! -d /run/log/journal ] || [ -L /run/log/journal ]; } && [ -s /mnt/root/etc/machine-id ]; then +if { [ ! -d /run/log/journal ] || [ -L /run/log/journal ]; } && [ -s "${ROOT_MOUNTPOINT}"/etc/machine-id ]; then # Only enter here if the directory does not exist or the location exists and is a symlink # (note that test -d /symlink-to-dir will return true) @@ -38,21 +41,21 @@ if { [ ! -d /run/log/journal ] || [ -L /run/log/journal ]; } && [ -s /mnt/root/e mkdir -p /run/log # Override the local machine-id - ln -sf /mnt/root/etc/machine-id /etc/machine-id + ln -sf "${ROOT_MOUNTPOINT}"/etc/machine-id /etc/machine-id # Remove the original link if it exists to avoid creating deep links [ -L /run/log/journal ] && rm /run/log/journal # If using persistent logging, the host will the journal under `/var/log/journal` # otherwise it will have it under /run/log/journal - [ -d "/mnt/root/run/log/journal/$(cat /etc/machine-id)" ] && ln -sf /mnt/root/run/log/journal /run/log/journal - [ -d "/mnt/root/var/log/journal/$(cat /etc/machine-id)" ] && ln -sf /mnt/root/var/log/journal /run/log/journal + [ -d "${ROOT_MOUNTPOINT}/run/log/journal/$(cat /etc/machine-id)" ] && ln -sf "${ROOT_MOUNTPOINT}"/run/log/journal /run/log/journal + [ -d "${ROOT_MOUNTPOINT}/var/log/journal/$(cat /etc/machine-id)" ] && ln -sf "${ROOT_MOUNTPOINT}"/var/log/journal /run/log/journal fi # Mount the host kernel module path onto the expected location # We need to do this as busybox doesn't support using a custom location if [ ! -d /lib/modules ]; then - ln -s /mnt/root/lib/modules /lib/modules + ln -s "${ROOT_MOUNTPOINT}"/lib/modules /lib/modules fi # Now load the ip6_tables kernel module, so we can do # filtering on ipv6 addresses. Don't fail here if the @@ -68,7 +71,7 @@ export LOCKFILE_UID=65534 # Cleanup leftover Supervisor-created lockfiles from any previous processes. # Supervisor-created lockfiles have a UID of 65534. -find "/mnt/root${BASE_LOCK_DIR}" -type f -user "${LOCKFILE_UID}" -name "*updates.lock" -delete || true +find "${ROOT_MOUNTPOINT}${BASE_LOCK_DIR}" -type f -user "${LOCKFILE_UID}" -name "*updates.lock" -delete || true if [ "${LIVEPUSH}" = "1" ]; then exec npx nodemon --watch src --watch typings --ignore tests -e js,ts,json \ diff --git a/mount-partitions.sh b/mount-partitions.sh new file mode 100755 index 00000000..c0d48330 --- /dev/null +++ b/mount-partitions.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Mounts boot, state, & data partitions from balenaOS. +# The container must be privileged for this to function correctly. + +# Get the current boot block device in case there are duplicate partition labels +# for `(balena|resin)-(boot|state|data)` found. +current_boot_block_device="" +if [ "${TEST}" != 1 ]; then + current_boot_partition=$(fdisk -l | grep '* ' | cut -d' ' -f1 2>&1) + current_boot_block_device=$(lsblk -no pkname "${current_boot_partition}") + if [ "${current_boot_block_device}" = "" ]; then + echo "ERROR: Could not determine boot device. Please launch Supervisor as a privileged container with host networking." + exit 1 + fi +fi + +# Mounts a device to a path if it's not already mounted. +# Usage: do_mount DEVICE MOUNT_PATH +do_mount() { + device=$1 + mount_path=$2 + + # Create the directory if it doesn't exist + mkdir -p "${mount_path}" + + # Mount the device if it doesn't exist + if [ "$(mountpoint -n "${mount_path}" | awk '{ print $1 }')" != "${device}" ]; then + mount "${device}" "${mount_path}" + fi +} + +# Find the devices for each balenaOS partition. +# Usage: setup_then_mount PARTITION MOUNT_PATH +# PARTITION should be one of boot, state, or data. +setup_then_mount() { + # If in test environment, pretend we've succeeded at mounting everything to their + # new mountpoints. We don't want to actually mount in a containerized test environment + # where the Supervisor is probably not running on a host that has the needed partitions. + if [ "${TEST}" = 1 ]; then + return 0 + fi + + partition_label=$1 + target_path=$2 + + # Get one or more devices matching label, accounting for legacy partition labels. + device=$(blkid | grep -E "(resin|balena)-${partition_label}" | awk -F':' '{print $1}') + + # If multiple devices with the partition label are found, mount to the device + # that's part of the current boot device, as this indicates a duplicate + # label somewhere created by a user or an inconsistency in the system. + # We've been able to identify the current boot device, so use that + # to find the device with the correct label amongst 2+ devices. + for d in ${device}; do + if [ "$(echo "$d" | grep "$current_boot_block_device")" != "" ]; then + echo "INFO: Found device $d on current boot device $current_boot_block_device, using as mount for '(resin|balena)-${partition_label}'." + do_mount "${d}" "${target_path}" + return 0 + fi + done + + # If no devices were found, use legacy mountpoints. + echo "ERROR: Could not determine which partition to mount for label '(resin|balena)-${partition_label}'. Please make sure the Supervisor is running on a balenaOS device." + exit 1 +} + +# Set overlayfs root mountpoint +export ROOT_MOUNTPOINT="/mnt/root" + +# Set boot mountpoint +BOOT_MOUNTPOINT="/mnt/boot" +setup_then_mount "boot" "${BOOT_MOUNTPOINT}" +export BOOT_MOUNTPOINT + +# Read from the os-release of boot partition instead of overlay +export HOST_OS_VERSION_PATH="${BOOT_MOUNTPOINT}/os-release" + +# CONFIG_MOUNT_POINT is set to /boot/config.json in Dockerfile.template, +# but that's a legacy mount provided by host and we should override it. +export CONFIG_MOUNT_POINT="${BOOT_MOUNTPOINT}/config.json" diff --git a/src/compose/app.ts b/src/compose/app.ts index 32742577..783ca49d 100644 --- a/src/compose/app.ts +++ b/src/compose/app.ts @@ -23,7 +23,7 @@ import * as config from '../config'; import { checkTruthy, checkString } from '../lib/validation'; import { ServiceComposeConfig, DeviceMetadata } from './types/service'; import { ImageInspectInfo } from 'dockerode'; -import { pathExistsOnHost } from '../lib/fs-utils'; +import { pathExistsOnRoot } from '../lib/host-utils'; import { isSupervisor } from '../lib/supervisor-metadata'; export interface AppConstructOpts { @@ -794,8 +794,8 @@ export class App { .getNetworkGateway(constants.supervisorNetworkInterface) .catch(() => '127.0.0.1'), (async () => ({ - firmware: await pathExistsOnHost('/lib/firmware'), - modules: await pathExistsOnHost('/lib/modules'), + firmware: await pathExistsOnRoot('/lib/firmware'), + modules: await pathExistsOnRoot('/lib/modules'), }))(), ( (await config.get('hostname')) ?? diff --git a/src/compose/service.ts b/src/compose/service.ts index 7072a1a6..174880ec 100644 --- a/src/compose/service.ts +++ b/src/compose/service.ts @@ -8,7 +8,7 @@ import { DockerPortOptions, PortMap } from './ports'; import * as ComposeUtils from './utils'; import * as updateLock from '../lib/update-lock'; import { sanitiseComposeConfig } from './sanitise'; -import { getPathOnHost } from '../lib/fs-utils'; +import { pathOnRoot } from '../lib/host-utils'; import log from '../lib/supervisor-console'; import * as conversions from '../lib/conversions'; import { checkInt } from '../lib/validation'; @@ -930,7 +930,7 @@ export class Service { this.appId || 0, this.serviceName || '', ); - return getPathOnHost( + return pathOnRoot( ...['handover-complete', 'resin-kill-me'].map((tail) => path.join(lockPath, tail), ), diff --git a/src/compose/volume-manager.ts b/src/compose/volume-manager.ts index f6ff61a7..195d545c 100644 --- a/src/compose/volume-manager.ts +++ b/src/compose/volume-manager.ts @@ -1,10 +1,10 @@ import * as _ from 'lodash'; -import * as Path from 'path'; +import * as path from 'path'; import { VolumeInspectInfo } from 'dockerode'; -import * as constants from '../lib/constants'; import { isNotFoundError, InternalInconsistencyError } from '../lib/errors'; import { safeRename } from '../lib/fs-utils'; +import { pathOnRoot } from '../lib/host-utils'; import { docker } from '../lib/docker-utils'; import * as LogTypes from '../lib/log-types'; import log from '../lib/supervisor-console'; @@ -97,10 +97,8 @@ export async function createFromPath( .getVolume(Volume.generateDockerName(volume.appId, volume.name)) .inspect(); - const volumePath = Path.join( - constants.rootMountPoint, - 'mnt/data', - ...inspect.Mountpoint.split(Path.sep).slice(3), + const volumePath = pathOnRoot( + path.join('mnt/data', ...inspect.Mountpoint.split(path.sep).slice(3)), ); await safeRename(oldPath, volumePath); diff --git a/src/config/backends/config-fs.ts b/src/config/backends/config-fs.ts index 896af224..f80da6ce 100644 --- a/src/config/backends/config-fs.ts +++ b/src/config/backends/config-fs.ts @@ -19,15 +19,10 @@ import log from '../../lib/supervisor-console'; type ConfigfsConfig = Dictionary; export class ConfigFs extends ConfigBackend { - private readonly SystemAmlFiles = path.join( - constants.rootMountPoint, - 'boot/acpi-tables', - ); + private readonly SystemAmlFiles = hostUtils.pathOnRoot('boot/acpi-tables'); private readonly ConfigFilePath = hostUtils.pathOnBoot('configfs.json'); - private readonly ConfigfsMountPoint = path.join( - constants.rootMountPoint, - 'sys/kernel/config', - ); + private readonly ConfigfsMountPoint = + hostUtils.pathOnRoot('sys/kernel/config'); private readonly ConfigVarNamePrefix = `${constants.hostConfigVarPrefix}CONFIGFS_`; // supported backend for the following device types... diff --git a/src/config/backends/config-txt.ts b/src/config/backends/config-txt.ts index 3ffbbdbf..939fd1d8 100644 --- a/src/config/backends/config-txt.ts +++ b/src/config/backends/config-txt.ts @@ -19,7 +19,7 @@ import * as hostUtils from '../../lib/host-utils'; export class ConfigTxt extends ConfigBackend { private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}CONFIG_`; - private static bootConfigPath = hostUtils.pathOnBoot(`config.txt`); + private static bootConfigPath = hostUtils.pathOnBoot('config.txt'); public static bootConfigVarRegex = new RegExp( '(?:' + _.escapeRegExp(ConfigTxt.bootConfigVarPrefix) + ')(.+)', diff --git a/src/config/backends/extra-uEnv.ts b/src/config/backends/extra-uEnv.ts index 73f88617..93cc32aa 100644 --- a/src/config/backends/extra-uEnv.ts +++ b/src/config/backends/extra-uEnv.ts @@ -38,7 +38,7 @@ const OPTION_REGEX = /^\s*(\w+)=(.*)$/; export class ExtraUEnv extends ConfigBackend { private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`; - private static bootConfigPath = hostUtils.pathOnBoot(`extra_uEnv.txt`); + private static bootConfigPath = hostUtils.pathOnBoot('extra_uEnv.txt'); private static entries: Record = { custom_fdt_file: { key: 'custom_fdt_file', collection: false }, diff --git a/src/config/backends/odmdata.ts b/src/config/backends/odmdata.ts index 6c7a72ae..7e03717c 100644 --- a/src/config/backends/odmdata.ts +++ b/src/config/backends/odmdata.ts @@ -5,6 +5,7 @@ import { ConfigOptions, ConfigBackend } from './backend'; import * as constants from '../../lib/constants'; import log from '../../lib/supervisor-console'; import { ODMDataError } from '../../lib/errors'; +import { pathOnRoot } from '../../lib/host-utils'; /** * A backend to handle ODMDATA configuration @@ -15,8 +16,10 @@ import { ODMDataError } from '../../lib/errors'; export class Odmdata extends ConfigBackend { private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}ODMDATA_`; - private static bootConfigPath = `${constants.rootMountPoint}/dev/mmcblk0boot0`; - private static bootConfigLockPath = `${constants.rootMountPoint}/sys/block/mmcblk0boot0/force_ro`; + private static bootConfigPath = pathOnRoot('/dev/mmcblk0boot0'); + private static bootConfigLockPath = pathOnRoot( + '/sys/block/mmcblk0boot0/force_ro', + ); private static supportedConfigs = ['configuration']; private BYTE_OFFSETS = [1659, 5243, 18043]; private CONFIG_BYTES = [ diff --git a/src/config/configJson.ts b/src/config/configJson.ts index d86963de..01c4c6fe 100644 --- a/src/config/configJson.ts +++ b/src/config/configJson.ts @@ -1,13 +1,9 @@ import * as Bluebird from 'bluebird'; import * as _ from 'lodash'; -import * as path from 'path'; import * as constants from '../lib/constants'; import * as hostUtils from '../lib/host-utils'; import * as osRelease from '../lib/os-release'; - -import log from '../lib/supervisor-console'; - import { readLock, writeLock } from '../lib/update-lock'; import * as Schema from './schema'; @@ -87,56 +83,27 @@ export default class ConfigJsonConfigBackend { private async write(): Promise { // writeToBoot uses fatrw to safely write to the boot partition - return hostUtils.writeToBoot( - await this.pathOnHost(), - JSON.stringify(this.cache), - ); + return hostUtils.writeToBoot(await this.path(), JSON.stringify(this.cache)); } private async read(): Promise { - const filename = await this.pathOnHost(); + const filename = await this.path(); return JSON.parse(await hostUtils.readFromBoot(filename, 'utf-8')); } - private async resolveConfigPath(): Promise { + private async path(): Promise { + // TODO: Remove this once api-binder tests are migrated. The only + // time configPath is passed to the constructor is in the legacy tests. if (this.configPath != null) { return this.configPath; } - if (constants.configJsonPathOnHost != null) { - return constants.configJsonPathOnHost; - } const osVersion = await osRelease.getOSVersion(constants.hostOSVersionPath); if (osVersion == null) { throw new Error('Failed to detect OS version!'); } - if (/^(Resin OS|balenaOS)/.test(osVersion)) { - // In Resin OS 1.12, $BOOT_MOUNTPOINT was added and it coincides with config.json's path. - if (constants.bootMountPointFromEnv != null) { - return path.join(constants.bootMountPointFromEnv, 'config.json'); - } - // Older 1.X versions have config.json here - return '/mnt/conf/config.json'; - } else { - // In non-balenaOS hosts (or older than 1.0.0), if CONFIG_JSON_PATH wasn't passed - // then we can't do atomic changes (only access to config.json we have is in /boot, - // which is assumed to be a file bind mount where rename is impossible). - throw new Error( - `OS version '${osVersion}' does not match any known balenaOS version.`, - ); - } - } - - private async pathOnHost(): Promise { - try { - return path.join( - constants.rootMountPoint, - await this.resolveConfigPath(), - ); - } catch (err) { - log.error('There was an error detecting the config.json path', err); - return constants.configJsonNonAtomicPath; - } + // The default path in the boot partition + return constants.configJsonPath; } } diff --git a/src/config/functions.ts b/src/config/functions.ts index 2f27ee6c..af3330cf 100644 --- a/src/config/functions.ts +++ b/src/config/functions.ts @@ -1,4 +1,3 @@ -import * as Bluebird from 'bluebird'; import * as _ from 'lodash'; import * as memoizee from 'memoizee'; @@ -13,7 +12,7 @@ import log from '../lib/supervisor-console'; export const fnSchema = { version: () => { - return Bluebird.resolve(supervisorVersion); + return Promise.resolve(supervisorVersion); }, currentApiKey: () => { return config diff --git a/src/device-config.ts b/src/device-config.ts index 4cd493d1..50ddd6be 100644 --- a/src/device-config.ts +++ b/src/device-config.ts @@ -16,6 +16,7 @@ import { matchesAnyBootConfig } from './config/backends'; import { ConfigBackend } from './config/backends/backend'; import { Odmdata } from './config/backends/odmdata'; import * as fsUtils from './lib/fs-utils'; +import { pathOnRoot } from './lib/host-utils'; const vpnServiceName = 'openvpn'; @@ -24,7 +25,7 @@ const vpnServiceName = 'openvpn'; // by some config changes, we leave this here for now. There is planned // functionality to allow image installs to require reboots, at that moment // this constant can be moved somewhere else -const REBOOT_BREADCRUMB = fsUtils.getPathOnHost( +const REBOOT_BREADCRUMB = pathOnRoot( '/tmp/balena-supervisor/reboot-after-apply', ); diff --git a/src/device-state/target-state.ts b/src/device-state/target-state.ts index 7281b4cb..8b75f635 100644 --- a/src/device-state/target-state.ts +++ b/src/device-state/target-state.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'events'; import * as url from 'url'; import { delay } from 'bluebird'; import * as _ from 'lodash'; -import Bluebird = require('bluebird'); +import * as Bluebird from 'bluebird'; import type StrictEventEmitter from 'strict-event-emitter-types'; import type { TargetState } from '../types/state'; diff --git a/src/host-config.ts b/src/host-config.ts index 52caf6d7..92acfc0a 100644 --- a/src/host-config.ts +++ b/src/host-config.ts @@ -5,11 +5,15 @@ import * as path from 'path'; import * as config from './config'; import * as applicationManager from './compose/application-manager'; -import * as constants from './lib/constants'; import * as dbus from './lib/dbus'; import { ENOENT } from './lib/errors'; import { mkdirp, unlinkAll } from './lib/fs-utils'; -import { writeToBoot, readFromBoot } from './lib/host-utils'; +import { + writeToBoot, + readFromBoot, + pathOnRoot, + pathOnBoot, +} from './lib/host-utils'; import * as updateLock from './lib/update-lock'; const redsocksHeader = stripIndent` @@ -30,11 +34,7 @@ const redsocksFooter = '}\n'; const proxyFields = ['type', 'ip', 'port', 'login', 'password']; -const proxyBasePath = path.join( - constants.rootMountPoint, - constants.bootMountPoint, - 'system-proxy', -); +const proxyBasePath = pathOnBoot('system-proxy'); const redsocksConfPath = path.join(proxyBasePath, 'redsocks.conf'); const noProxyPath = path.join(proxyBasePath, 'no_proxy'); @@ -178,7 +178,7 @@ async function setProxy(maybeConf: ProxyConfig | null): Promise { } } -const hostnamePath = path.join(constants.rootMountPoint, '/etc/hostname'); +const hostnamePath = pathOnRoot('/etc/hostname'); async function readHostname() { const hostnameData = await fs.readFile(hostnamePath, 'utf-8'); return _.trim(hostnameData); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index adf193e8..a1c08ef9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,19 +1,38 @@ import * as path from 'path'; import { checkString } from './validation'; -const bootMountPointFromEnv = checkString(process.env.BOOT_MOUNTPOINT); -const rootMountPoint = checkString(process.env.ROOT_MOUNTPOINT) || '/mnt/root'; - const supervisorNetworkInterface = 'supervisor0'; +// /mnt/root is the legacy root mountpoint +const rootMountPoint = checkString(process.env.ROOT_MOUNTPOINT) || '/mnt/root'; +const withRootMount = (p: string) => path.join(rootMountPoint, p); +const bootMountPoint = checkString(process.env.BOOT_MOUNTPOINT) || '/mnt/boot'; + const constants = { + // Root overlay paths rootMountPoint, - stateMountPoint: '/mnt/state', + vpnStatusPath: + checkString(process.env.VPN_STATUS_PATH) || + withRootMount('/run/openvpn/vpn_status'), + // Boot paths + bootMountPoint, + configJsonPath: + checkString(process.env.CONFIG_MOUNT_POINT) || + path.join(bootMountPoint, 'config.json'), + hostOSVersionPath: + checkString(process.env.HOST_OS_VERSION_PATH) || + withRootMount('/etc/os-release'), + // Data paths databasePath: checkString(process.env.DATABASE_PATH) || '/data/database.sqlite', + appsJsonPath: + process.env.APPS_JSON_PATH || withRootMount('/mnt/data/apps.json'), + migrationBackupFile: 'backup.tgz', + // State paths + stateMountPoint: '/mnt/state', + // Other constants: network, Engine, /sys containerId: checkString(process.env.SUPERVISOR_CONTAINER_ID) || undefined, dockerSocket: process.env.DOCKER_SOCKET || '/var/run/docker.sock', - // In-container location for docker socket // Mount in /host/run to avoid clashing with systemd containerDockerSocket: '/host/run/balena-engine.sock', @@ -21,12 +40,6 @@ const constants = { checkString(process.env.SUPERVISOR_IMAGE) || 'resin/rpi-supervisor', ledFile: checkString(process.env.LED_FILE) || '/sys/class/leds/led0/brightness', - vpnStatusPath: - checkString(process.env.VPN_STATUS_PATH) || - `${rootMountPoint}/run/openvpn/vpn_status`, - hostOSVersionPath: - checkString(process.env.HOST_OS_VERSION_PATH) || - `${rootMountPoint}/etc/os-release`, macAddressPath: checkString(process.env.MAC_ADDRESS_PATH) || `/sys/class/net`, privateAppEnvVars: [ 'RESIN_SUPERVISOR_API_KEY', @@ -34,10 +47,6 @@ const constants = { 'BALENA_SUPERVISOR_API_KEY', 'BALENA_API_KEY', ], - bootMountPointFromEnv, - bootMountPoint: bootMountPointFromEnv || '/boot', - configJsonPathOnHost: checkString(process.env.CONFIG_JSON_PATH), - configJsonNonAtomicPath: '/boot/config.json', supervisorNetworkInterface, allowedInterfaces: [ 'resin-vpn', @@ -46,9 +55,6 @@ const constants = { 'lo', supervisorNetworkInterface, ], - appsJsonPath: - process.env.APPS_JSON_PATH || - path.join(rootMountPoint, '/mnt/data', 'apps.json'), ipAddressUpdateInterval: 30 * 1000, imageCleanupErrorIgnoreTimeout: 3600 * 1000, maxDeltaDownloads: 3, @@ -57,7 +63,6 @@ const constants = { }, bootBlockDevice: '/dev/mmcblk0p1', hostConfigVarPrefix: 'HOST_', - migrationBackupFile: 'backup.tgz', // Use this failure multiplied by 2**Number of failures to increase // the backoff on subsequent failures backoffIncrement: 500, diff --git a/src/lib/fs-utils.ts b/src/lib/fs-utils.ts index b4cdc7db..612be6ef 100644 --- a/src/lib/fs-utils.ts +++ b/src/lib/fs-utils.ts @@ -5,8 +5,6 @@ import { exec as execSync } from 'child_process'; import { promisify } from 'util'; import { uptime } from 'os'; -import * as constants from './constants'; - export const exec = promisify(execSync); export async function writeAndSyncFile( @@ -47,14 +45,6 @@ export async function exists(p: string): Promise { } } -/** - * Check if a path exists as a direct child of the device's root mountpoint, - * which is equal to constants.rootMountPoint (`/mnt/root`). - */ -export function pathExistsOnHost(pathName: string): Promise { - return exists(path.join(constants.rootMountPoint, pathName)); -} - /** * Recursively create directories until input directory. * Equivalent to mkdirp package, which uses this under the hood. @@ -77,19 +67,6 @@ export async function unlinkAll(...paths: string[]): Promise { ); } -/** - * Get one or more paths as they exist in relation to host OS's root. - */ -export function getPathOnHost(path: string): string; -export function getPathOnHost(...paths: string[]): string[]; -export function getPathOnHost(...paths: string[]): string[] | string { - if (paths.length === 1) { - return path.join(constants.rootMountPoint, paths[0]); - } else { - return paths.map((p: string) => path.join(constants.rootMountPoint, p)); - } -} - /** * Change modification and access time of the given file. * It creates an empty file if it does not exist diff --git a/src/lib/host-utils.ts b/src/lib/host-utils.ts index 34542dcf..ab170d85 100644 --- a/src/lib/host-utils.ts +++ b/src/lib/host-utils.ts @@ -3,17 +3,32 @@ import * as path from 'path'; import * as constants from './constants'; import { exec, exists } from './fs-utils'; +function withBase(base: string) { + function withPath(): string; + function withPath(path: string): string; + function withPath(...paths: string[]): string[]; + function withPath(...paths: string[]): string[] | string { + if (arguments.length === 0) { + return base; + } else if (paths.length === 1) { + return path.join(base, paths[0]); + } else { + return paths.map((p: string) => path.join(base, p)); + } + } + return withPath; +} + // Returns an absolute path starting from the hostOS root partition // This path is accessible from within the Supervisor container -export function pathOnRoot(relPath: string) { - return path.join(constants.rootMountPoint, relPath); -} +export const pathOnRoot = withBase(constants.rootMountPoint); + +export const pathExistsOnRoot = async (p: string) => + await exists(pathOnRoot(p)); // Returns an absolute path starting from the hostOS boot partition // This path is accessible from within the Supervisor container -export function pathOnBoot(relPath: string) { - return pathOnRoot(path.join(constants.bootMountPoint, relPath)); -} +export const pathOnBoot = withBase(constants.bootMountPoint); class CodedError extends Error { constructor(msg: string, readonly code: number | string) { @@ -21,7 +36,7 @@ class CodedError extends Error { } } -// Receives an absolute path for a file (assumed to be under the boot partition, e.g. `/mnt/root/mnt/boot/config.txt`) +// Receives an absolute path for a file (assumed to be under the boot partition, e.g. `/mnt/boot/config.txt`) // and reads from the given location. This function uses fatrw to safely read from a FAT filesystem // https://github.com/balena-os/fatrw export async function readFromBoot( @@ -51,7 +66,8 @@ export async function readFromBoot( } } -// Receives an absolute path for a file (assumed to be under the boot partition, e.g. `/mnt/root/mnt/boot/config.txt`) +// Receives an absolute path for a file (assumed to be under the boot partition, +// e.g. `/mnt/boot/config.txt` or legacy `/mnt/root/mnt/boot/config.txt`) // and writes the given data. This function uses fatrw to safely write from a FAT filesystem // https://github.com/balena-os/fatrw export async function writeToBoot(fileName: string, data: string | Buffer) { diff --git a/src/lib/legacy.ts b/src/lib/legacy.ts index e4736a2b..d40af222 100644 --- a/src/lib/legacy.ts +++ b/src/lib/legacy.ts @@ -12,10 +12,10 @@ import { DatabaseParseError, isNotFoundError, InternalInconsistencyError, -} from '../lib/errors'; -import * as constants from '../lib/constants'; -import { docker } from '../lib/docker-utils'; -import { log } from '../lib/supervisor-console'; +} from './errors'; +import { docker } from './docker-utils'; +import { log } from './supervisor-console'; +import { pathOnRoot } from './host-utils'; import Volume from '../compose/volume'; import * as logger from '../logger'; import type { @@ -35,10 +35,8 @@ async function createVolumeFromLegacyData( appUuid: string, ): Promise { const name = defaultLegacyVolume(); - const legacyPath = path.join( - constants.rootMountPoint, - 'mnt/data/resin-data', - appId.toString(), + const legacyPath = pathOnRoot( + path.join('mnt/data/resin-data', appId.toString()), ); try { diff --git a/src/lib/migration.ts b/src/lib/migration.ts index 73c505fb..4d8fa0c8 100644 --- a/src/lib/migration.ts +++ b/src/lib/migration.ts @@ -2,28 +2,25 @@ import * as Bluebird from 'bluebird'; import * as _ from 'lodash'; import { promises as fs } from 'fs'; import * as path from 'path'; -import * as rimraf from 'rimraf'; - -const rimrafAsync = Bluebird.promisify(rimraf); import * as volumeManager from '../compose/volume-manager'; import * as deviceState from '../device-state'; -import * as constants from '../lib/constants'; -import { BackupError, isNotFoundError } from '../lib/errors'; -import { exec, pathExistsOnHost, mkdirp } from '../lib/fs-utils'; -import { log } from '../lib/supervisor-console'; - import { TargetState } from '../types'; +import * as constants from './constants'; +import { BackupError, isNotFoundError } from './errors'; +import { exec, exists, mkdirp, unlinkAll } from './fs-utils'; +import { log } from './supervisor-console'; +import { pathOnRoot } from './host-utils'; export async function loadBackupFromMigration( targetState: TargetState, retryDelay: number, ): Promise { try { - const exists = await pathExistsOnHost( - path.join('mnt/data', constants.migrationBackupFile), + const migrationExists = await exists( + pathOnRoot(path.join('mnt/data', constants.migrationBackupFile)), ); - if (!exists) { + if (!migrationExists) { return; } log.info('Migration backup detected'); @@ -42,12 +39,12 @@ export async function loadBackupFromMigration( const volumes = release?.volumes ?? {}; - const backupPath = path.join(constants.rootMountPoint, 'mnt/data/backup'); + const backupPath = pathOnRoot('mnt/data/backup'); // We clear this path in case it exists from an incomplete run of this function - await rimrafAsync(backupPath); + await unlinkAll(backupPath); await mkdirp(backupPath); await exec(`tar -xzf backup.tgz -C ${backupPath}`, { - cwd: path.join(constants.rootMountPoint, 'mnt/data'), + cwd: pathOnRoot('mnt/data'), }); for (const volumeName of await fs.readdir(backupPath)) { @@ -86,13 +83,9 @@ export async function loadBackupFromMigration( } } - await rimrafAsync(backupPath); - await rimrafAsync( - path.join( - constants.rootMountPoint, - 'mnt/data', - constants.migrationBackupFile, - ), + await unlinkAll(backupPath); + await unlinkAll( + pathOnRoot(path.join('mnt/data', constants.migrationBackupFile)), ); } catch (err) { log.error(`Error restoring migration backup, retrying: ${err}`); diff --git a/src/lib/update-lock.ts b/src/lib/update-lock.ts index 54f1de20..93a21310 100644 --- a/src/lib/update-lock.ts +++ b/src/lib/update-lock.ts @@ -11,7 +11,8 @@ import { UpdatesLockedError, InternalInconsistencyError, } from './errors'; -import { getPathOnHost, pathExistsOnHost } from './fs-utils'; +import { exists } from './fs-utils'; +import { pathOnRoot } from './host-utils'; import * as config from '../config'; import * as lockfile from './lockfile'; import { NumericIdentifier } from '../types'; @@ -27,7 +28,7 @@ export function lockPath(appId: number, serviceName?: string): string { } function lockFilesOnHost(appId: number, serviceName: string): string[] { - return getPathOnHost( + return pathOnRoot( ...['updates.lock', 'resin-updates.lock'].map((filename) => path.join(lockPath(appId), serviceName, filename), ), @@ -48,10 +49,10 @@ export function abortIfHUPInProgress({ return Promise.all( ['rollback-health-breadcrumb', 'rollback-altboot-breadcrumb'].map( (filename) => - pathExistsOnHost(path.join(constants.stateMountPoint, filename)), + exists(pathOnRoot(path.join(constants.stateMountPoint, filename))), ), ).then((existsArray) => { - const anyExists = existsArray.some((exists) => exists); + const anyExists = existsArray.some((e) => e); if (anyExists && !force) { throw new UpdatesLockedError('Waiting for Host OS update to finish'); } @@ -119,7 +120,7 @@ export async function lock( const releases = new Map void>(); try { for (const id of sortedIds) { - const lockDir = getPathOnHost(lockPath(id)); + const lockDir = pathOnRoot(lockPath(id)); // Acquire write lock for appId releases.set(id, await writeLock(id)); // Get list of service folders in lock directory diff --git a/test/data/root/etc/os-release b/test/data/root/mnt/boot/os-release similarity index 100% rename from test/data/root/etc/os-release rename to test/data/root/mnt/boot/os-release diff --git a/test/integration/config.spec.ts b/test/integration/config.spec.ts index 9c91d8f4..994260e3 100644 --- a/test/integration/config.spec.ts +++ b/test/integration/config.spec.ts @@ -1,28 +1,18 @@ import * as _ from 'lodash'; -import * as path from 'path'; import { promises as fs } from 'fs'; import { SinonSpy, spy, stub } from 'sinon'; import { expect } from 'chai'; import { testfs, TestFs } from 'mocha-pod'; -import * as hostUtils from '~/lib/host-utils'; -import * as constants from '~/lib/constants'; import { fnSchema } from '~/src/config/functions'; +import * as hostUtils from '~/lib/host-utils'; +import { configJsonPath } from '~/lib/constants'; // Utility method to use along with `require` type Config = typeof import('~/src/config'); describe('config', () => { - const configJsonPath = path.join( - constants.rootMountPoint, - constants.bootMountPoint, - 'config.json', - ); - const deviceTypeJsonPath = path.join( - constants.rootMountPoint, - constants.bootMountPoint, - 'device-type.json', - ); + const deviceTypeJsonPath = hostUtils.pathOnBoot('device-type.json'); const readConfigJson = () => fs.readFile(configJsonPath, 'utf8').then((data) => JSON.parse(data)); @@ -154,7 +144,7 @@ describe('config', () => { // this is being skipped until the config module can be refactored it.skip('deduces OS variant from developmentMode if not set', async () => { const tFs = await testfs({ - '/mnt/root/etc/os-release': testfs.from( + [hostUtils.pathOnBoot('os-release')]: testfs.from( 'test/data/etc/os-release-novariant', ), }).enable(); diff --git a/test/integration/config/extra-uenv.spec.ts b/test/integration/config/extra-uenv.spec.ts index 667edb63..8118f0a5 100644 --- a/test/integration/config/extra-uenv.spec.ts +++ b/test/integration/config/extra-uenv.spec.ts @@ -27,7 +27,7 @@ describe('config/extra-uEnv', () => { it('should only parse supported configuration options from bootConfigPath', async () => { let tfs = await testfs({ - [hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent` + [hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent` custom_fdt_file=mycustom.dtb extra_os_cmdline=isolcpus=3,4 `, @@ -42,7 +42,7 @@ describe('config/extra-uEnv', () => { // Add other options that will get filtered out because they aren't supported tfs = await testfs({ - [hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent` + [hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent` custom_fdt_file=mycustom.dtb extra_os_cmdline=isolcpus=3,4 console=tty0 splash `, @@ -57,7 +57,7 @@ describe('config/extra-uEnv', () => { // Configuration with no supported values tfs = await testfs({ - [hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent` + [hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent` fdt=something_else isolcpus 123.12=5 @@ -71,7 +71,7 @@ describe('config/extra-uEnv', () => { it('only matches supported devices', async () => { // The file exists before const tfs = await testfs({ - [hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent` + [hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent` custom_fdt_file=mycustom.dtb extra_os_cmdline=isolcpus=3,4 `, @@ -87,7 +87,7 @@ describe('config/extra-uEnv', () => { // The file no longer exists await expect( - fs.access(hostUtils.pathOnBoot(`extra_uEnv.txt`)), + fs.access(hostUtils.pathOnBoot('extra_uEnv.txt')), 'extra_uEnv.txt does not exist before the test', ).to.be.rejected; for (const device of MATCH_TESTS) { @@ -99,7 +99,7 @@ describe('config/extra-uEnv', () => { it('errors when cannot find extra_uEnv.txt', async () => { // The file no longer exists await expect( - fs.access(hostUtils.pathOnBoot(`extra_uEnv.txt`)), + fs.access(hostUtils.pathOnBoot('extra_uEnv.txt')), 'extra_uEnv.txt does not exist before the test', ).to.be.rejected; await expect(backend.getBootConfig()).to.eventually.be.rejectedWith( @@ -111,7 +111,7 @@ describe('config/extra-uEnv', () => { for (const badConfig of MALFORMED_CONFIGS) { // Setup the environment with a bad config const tfs = await testfs({ - [hostUtils.pathOnBoot(`extra_uEnv.txt`)]: badConfig.contents, + [hostUtils.pathOnBoot('extra_uEnv.txt')]: badConfig.contents, }).enable(); // Expect warning log from the given bad config @@ -127,7 +127,7 @@ describe('config/extra-uEnv', () => { // This config contains a value set from something else // We to make sure the Supervisor is enforcing the source of truth (the cloud) // So after setting new values this unsupported/not set value should be gone - [hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent` + [hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent` extra_os_cmdline=rootwait isolcpus=3,4 other_service=set_this_value `, @@ -142,7 +142,7 @@ describe('config/extra-uEnv', () => { // Confirm that the file was written correctly await expect( - fs.readFile(hostUtils.pathOnBoot(`extra_uEnv.txt`), 'utf8'), + fs.readFile(hostUtils.pathOnBoot('extra_uEnv.txt'), 'utf8'), ).to.eventually.equal( 'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2\n', ); @@ -167,7 +167,7 @@ describe('config/extra-uEnv', () => { }; const tfs = await testfs({ - [hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent` + [hostUtils.pathOnBoot('extra_uEnv.txt')]: stripIndent` other_service=set_this_value `, }).enable(); @@ -181,7 +181,7 @@ describe('config/extra-uEnv', () => { }); await expect( - fs.readFile(hostUtils.pathOnBoot(`extra_uEnv.txt`), 'utf8'), + fs.readFile(hostUtils.pathOnBoot('extra_uEnv.txt'), 'utf8'), ).to.eventually.equal( 'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2 console=tty0 splash\n', ); diff --git a/test/integration/device-config.spec.ts b/test/integration/device-config.spec.ts index 9082ccc8..2997c126 100644 --- a/test/integration/device-config.spec.ts +++ b/test/integration/device-config.spec.ts @@ -12,7 +12,11 @@ import { ConfigTxt } from '~/src/config/backends/config-txt'; import { Odmdata } from '~/src/config/backends/odmdata'; import { ConfigFs } from '~/src/config/backends/config-fs'; import { SplashImage } from '~/src/config/backends/splash-image'; -import * as constants from '~/lib/constants'; +import { pathOnBoot, pathOnRoot } from '~/lib/host-utils'; +import { + configJsonPath as configJson, + hostOSVersionPath as osRelease, +} from '~/src/lib/constants'; import { testfs } from 'mocha-pod'; @@ -26,15 +30,9 @@ const splashImageBackend = new SplashImage(); // these tests could probably be removed if each backend has its own // test and the src/config/utils module is properly tested. describe('device-config', () => { - const bootMountPoint = path.join( - constants.rootMountPoint, - constants.bootMountPoint, - ); - const configJson = path.join(bootMountPoint, 'config.json'); - const configFsJson = path.join(bootMountPoint, 'configfs.json'); - const configTxt = path.join(bootMountPoint, 'config.txt'); - const deviceTypeJson = path.join(bootMountPoint, 'device-type.json'); - const osRelease = path.join(constants.rootMountPoint, '/etc/os-release'); + const configFsJson = pathOnBoot('configfs.json'); + const configTxt = pathOnBoot('config.txt'); + const deviceTypeJson = pathOnBoot('device-type.json'); let logSpy: SinonSpy; @@ -114,7 +112,9 @@ describe('device-config', () => { [osRelease]: stripIndent` PRETTY_NAME="balenaOS 2.88.5+rev1" META_BALENA_VERSION="2.88.5" - VARIANT_ID="dev"`, + VERSION="2.88.5+rev1" + VARIANT_ID="dev" + `, [deviceTypeJson]: JSON.stringify({ slug: 'fincm3', arch: 'armv7hf', @@ -311,7 +311,7 @@ describe('device-config', () => { }); describe('extlinux', () => { - const extlinuxConf = path.join(bootMountPoint, 'extlinux/extlinux.conf'); + const extlinuxConf = pathOnBoot('extlinux/extlinux.conf'); const tFs = testfs({ // This is only needed so config.get doesn't fail @@ -319,6 +319,7 @@ describe('device-config', () => { [osRelease]: stripIndent` PRETTY_NAME="balenaOS 2.88.5+rev1" META_BALENA_VERSION="2.88.5" + VERSION="2.88.5+rev1" VARIANT_ID="dev" `, [deviceTypeJson]: JSON.stringify({ @@ -456,11 +457,8 @@ describe('device-config', () => { }); describe('config-fs', () => { - const acpiTables = path.join(constants.rootMountPoint, 'boot/acpi-tables'); - const sysKernelAcpiTable = path.join( - constants.rootMountPoint, - 'sys/kernel/config/acpi/table', - ); + const acpiTables = pathOnRoot('boot/acpi-tables'); + const sysKernelAcpiTable = pathOnRoot('sys/kernel/config/acpi/table'); const tFs = testfs({ // This is only needed so config.get doesn't fail @@ -468,6 +466,7 @@ describe('device-config', () => { [osRelease]: stripIndent` PRETTY_NAME="balenaOS 2.88.5+rev1" META_BALENA_VERSION="2.88.5" + VERSION="2.88.5+rev1" VARIANT_ID="dev" `, [configFsJson]: JSON.stringify({ @@ -619,7 +618,7 @@ describe('device-config', () => { 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII='; const uri = `data:image/png;base64,${png}`; - const splash = path.join(bootMountPoint, 'splash'); + const splash = pathOnBoot('splash'); const mockFs = testfs( { @@ -628,6 +627,7 @@ describe('device-config', () => { [osRelease]: stripIndent` PRETTY_NAME="balenaOS 2.88.5+rev1" META_BALENA_VERSION="2.88.5" + VERSION="2.88.5+rev1" VARIANT_ID="dev" `, [deviceTypeJson]: JSON.stringify({ @@ -741,7 +741,7 @@ describe('device-config', () => { }); describe('getRequiredSteps', () => { - const splash = path.join(bootMountPoint, 'splash/balena-logo.png'); + const splash = pathOnBoot('splash/balena-logo.png'); const tFs = testfs({ // This is only needed so config.get doesn't fail @@ -752,6 +752,7 @@ describe('device-config', () => { [osRelease]: stripIndent` PRETTY_NAME="balenaOS 2.88.5+rev1" META_BALENA_VERSION="2.88.5" + VERSION="2.88.5+rev1" VARIANT_ID="dev" `, [deviceTypeJson]: JSON.stringify({ diff --git a/test/integration/host-config.spec.ts b/test/integration/host-config.spec.ts index 965f3e69..852b0e6d 100644 --- a/test/integration/host-config.spec.ts +++ b/test/integration/host-config.spec.ts @@ -8,10 +8,10 @@ import * as hostConfig from '~/src/host-config'; import * as config from '~/src/config'; import * as applicationManager from '~/src/compose/application-manager'; import { InstancedAppState } from '~/src/types/state'; -import * as constants from '~/lib/constants'; import * as updateLock from '~/lib/update-lock'; import { UpdatesLockedError } from '~/lib/errors'; import * as dbus from '~/lib/dbus'; +import { pathOnBoot, pathOnRoot } from '~/lib/host-utils'; import { createApps, createService, @@ -23,18 +23,11 @@ describe('host-config', () => { let currentApps: InstancedAppState; const APP_ID = 1; const SERVICE_NAME = 'one'; - const proxyBase = path.join( - constants.rootMountPoint, - constants.bootMountPoint, - 'system-proxy', - ); + const proxyBase = pathOnBoot('system-proxy'); const redsocksConf = path.join(proxyBase, 'redsocks.conf'); const noProxy = path.join(proxyBase, 'no_proxy'); - const hostname = path.join(constants.rootMountPoint, '/etc/hostname'); - const appLockDir = path.join( - constants.rootMountPoint, - updateLock.lockPath(APP_ID), - ); + const hostname = pathOnRoot('/etc/hostname'); + const appLockDir = pathOnRoot(updateLock.lockPath(APP_ID)); before(async () => { await config.initialized(); diff --git a/test/integration/lib/update-lock.spec.ts b/test/integration/lib/update-lock.spec.ts index c13f4af6..6f5df437 100644 --- a/test/integration/lib/update-lock.spec.ts +++ b/test/integration/lib/update-lock.spec.ts @@ -4,10 +4,10 @@ import { promises as fs } from 'fs'; import { testfs } from 'mocha-pod'; import * as updateLock from '~/lib/update-lock'; -import * as constants from '~/lib/constants'; import { UpdatesLockedError } from '~/lib/errors'; import * as config from '~/src/config'; import * as lockfile from '~/lib/lockfile'; +import { pathOnRoot } from '~/lib/host-utils'; describe('lib/update-lock', () => { describe('abortIfHUPInProgress', () => { @@ -16,10 +16,7 @@ describe('lib/update-lock', () => { 'rollback-altboot-breadcrumb', ]; - const breadcrumbsDir = path.join( - constants.rootMountPoint, - constants.stateMountPoint, - ); + const breadcrumbsDir = pathOnRoot('/mnt/state'); const createBreadcrumb = (breadcrumb: string) => testfs({ @@ -87,10 +84,7 @@ describe('lib/update-lock', () => { }; const lockdir = (appId: number, serviceName: string): string => - path.join( - constants.rootMountPoint, - updateLock.lockPath(appId, serviceName), - ); + pathOnRoot(updateLock.lockPath(appId, serviceName)); const expectLocks = async ( exists: boolean, diff --git a/test/legacy/fixtures.ts b/test/legacy/fixtures.ts index 09bbbed0..31a74e89 100644 --- a/test/legacy/fixtures.ts +++ b/test/legacy/fixtures.ts @@ -1,6 +1,5 @@ process.env.ROOT_MOUNTPOINT = './test/data'; process.env.BOOT_MOUNTPOINT = '/mnt/boot'; -process.env.CONFIG_JSON_PATH = '/config.json'; process.env.CONFIG_MOUNT_POINT = './test/data/config.json'; process.env.DATABASE_PATH = './test/data/database.sqlite'; process.env.DATABASE_PATH_2 = './test/data/database2.sqlite'; diff --git a/test/unit/lib/fs-utils.spec.ts b/test/unit/lib/fs-utils.spec.ts index c6b40b33..2fc28b5b 100644 --- a/test/unit/lib/fs-utils.spec.ts +++ b/test/unit/lib/fs-utils.spec.ts @@ -5,13 +5,13 @@ import { spy, SinonSpy } from 'sinon'; import mock = require('mock-fs'); import * as fsUtils from '~/lib/fs-utils'; -import { rootMountPoint } from '~/lib/constants'; +import { pathOnRoot } from '~/lib/host-utils'; describe('lib/fs-utils', () => { const testFileName1 = 'file.1'; const testFileName2 = 'file.2'; - const testFile1 = path.join(rootMountPoint, testFileName1); - const testFile2 = path.join(rootMountPoint, testFileName2); + const testFile1 = pathOnRoot(testFileName1); + const testFile2 = pathOnRoot(testFileName2); const mockFs = () => { mock({ @@ -75,7 +75,7 @@ describe('lib/fs-utils', () => { it('should rename a file', async () => { await fsUtils.safeRename(testFile1, testFile1 + 'rename'); - const dirContents = await fs.readdir(rootMountPoint); + const dirContents = await fs.readdir(pathOnRoot()); expect(dirContents).to.have.length(2); expect(dirContents).to.not.include(testFileName1); expect(dirContents).to.include(testFileName1 + 'rename'); @@ -83,7 +83,7 @@ describe('lib/fs-utils', () => { it('should replace an existing file', async () => { await fsUtils.safeRename(testFile1, testFile2); - const dirContents = await fs.readdir(rootMountPoint); + const dirContents = await fs.readdir(pathOnRoot()); expect(dirContents).to.have.length(1); expect(dirContents).to.include(testFileName2); expect(dirContents).to.not.include(testFileName1); @@ -103,28 +103,14 @@ describe('lib/fs-utils', () => { }); }); - describe('pathExistsOnHost', () => { - before(mockFs); - after(unmockFs); - - it('should return whether a file exists in host OS fs', async () => { - expect(await fsUtils.pathExistsOnHost(testFileName1)).to.be.true; - await fs.unlink(testFile1); - expect(await fsUtils.pathExistsOnHost(testFileName1)).to.be.false; - }); - }); - describe('mkdirp', () => { before(mockFs); after(unmockFs); it('should recursively create directories', async () => { - await fsUtils.mkdirp( - path.join(rootMountPoint, 'test1', 'test2', 'test3'), - ); - expect(() => - fs.readdir(path.join(rootMountPoint, 'test1', 'test2', 'test3')), - ).to.not.throw(); + const directory = path.join(pathOnRoot('test1'), 'test2', 'test3'); + await fsUtils.mkdirp(directory); + expect(() => fs.readdir(directory)).to.not.throw(); }); }); @@ -134,24 +120,12 @@ describe('lib/fs-utils', () => { it('should unlink a single file', async () => { await fsUtils.unlinkAll(testFile1); - expect(await fs.readdir(rootMountPoint)).to.not.include(testFileName1); + expect(await fs.readdir(pathOnRoot())).to.not.include(testFileName1); }); it('should unlink multiple files', async () => { await fsUtils.unlinkAll(testFile1, testFile2); - expect(await fs.readdir(rootMountPoint)).to.have.length(0); - }); - }); - - describe('getPathOnHost', () => { - before(mockFs); - after(unmockFs); - - it("should return the paths of one or more files as they exist on host OS's root", async () => { - expect(fsUtils.getPathOnHost(testFileName1)).to.deep.equal(testFile1); - expect(fsUtils.getPathOnHost(testFileName1, testFileName2)).to.deep.equal( - [testFile1, testFile2], - ); + expect(await fs.readdir(pathOnRoot())).to.have.length(0); }); });