mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-20 09:46:19 +00:00
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:
parent
9522c15ecd
commit
49ee1042a8
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
25
entry.sh
25
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 \
|
||||
|
81
mount-partitions.sh
Executable file
81
mount-partitions.sh
Executable 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"
|
@ -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')) ??
|
||||
|
@ -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),
|
||||
),
|
||||
|
@ -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);
|
||||
|
@ -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...
|
||||
|
@ -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) + ')(.+)',
|
||||
|
@ -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 },
|
||||
|
@ -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 = [
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
);
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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}`);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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',
|
||||
);
|
||||
|
@ -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({
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user