Mount boot partition into container on Supervisor start

As the Supervisor is a privileged container, it has access to host /dev, and therefore has access
to boot, data, and state balenaOS partitions. This commit sets up the framework for the following:

- Finds the /dev partition that corresponds to each partition based on partition label
- Mounts the partitions into set mountpoints in the device
- Removes reliance on env vars and mountpoints provided by host's start-balena-supervisor script
- Simplifies host path querying by centralizing these queries through methods in lib/host-utils.ts

This particular changes env vars for and mounts the boot partition.

Since the Supervisor would no longer rely on container `run` arguments provided by a host script,
this change moves Supervisor closer to being able to start itself (Supervisor-as-an-app).

Change-type: minor
Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang 2023-02-20 22:11:27 -08:00 committed by Christina W
parent 9522c15ecd
commit 49ee1042a8
31 changed files with 285 additions and 290 deletions

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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 \

81
mount-partitions.sh Executable file
View File

@ -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"

View File

@ -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')) ??

View File

@ -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),
),

View File

@ -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);

View File

@ -19,15 +19,10 @@ import log from '../../lib/supervisor-console';
type ConfigfsConfig = Dictionary<string[]>;
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...

View File

@ -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) + ')(.+)',

View File

@ -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<EntryKey, Entry> = {
custom_fdt_file: { key: 'custom_fdt_file', collection: false },

View File

@ -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 = [

View File

@ -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<void> {
// 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<string> {
const filename = await this.pathOnHost();
const filename = await this.path();
return JSON.parse(await hostUtils.readFromBoot(filename, 'utf-8'));
}
private async resolveConfigPath(): Promise<string> {
private async path(): Promise<string> {
// 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<string> {
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;
}
}

View File

@ -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

View File

@ -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',
);

View File

@ -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';

View File

@ -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<void> {
}
}
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);

View File

@ -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,

View File

@ -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<boolean> {
}
}
/**
* 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<boolean> {
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<void> {
);
}
/**
* 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

View File

@ -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) {

View File

@ -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<Volume | void> {
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 {

View File

@ -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<void> {
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}`);

View File

@ -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<T extends unknown>(
const releases = new Map<number, () => 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

View File

@ -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();

View File

@ -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',
);

View File

@ -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({

View File

@ -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();

View File

@ -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,

View File

@ -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';

View File

@ -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);
});
});