Mount data and state partitions on container startup

Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang 2023-02-20 22:46:15 -08:00 committed by Christina W
parent 49ee1042a8
commit 4c948c8854
12 changed files with 60 additions and 31 deletions

View File

@ -22,6 +22,6 @@ testfs:
# when restoring the filesystem
cleanup:
- /data/database.sqlite
- /data/apps.json.preloaded
- /mnt/data/apps.json.preloaded
- /mnt/root/tmp/balena-supervisor/**/*.lock
- /mnt/boot/splash/*.png

View File

@ -101,6 +101,9 @@ services:
ROOT_MOUNTPOINT: /mnt/root
BOOT_MOUNTPOINT: /mnt/boot
HOST_OS_VERSION_PATH: /mnt/boot/os-release
STATE_MOUNTPOINT: /mnt/state
DATA_MOUNTPOINT: /mnt/data
DATABASE_PATH: /data/database.sqlite
# 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
@ -109,6 +112,8 @@ services:
- /data
- /mnt/root
- /mnt/boot
- /mnt/state
- /mnt/data
volumes:
dbus:

View File

@ -33,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 "${ROOT_MOUNTPOINT}"/etc/machine-id ]; then
if { [ ! -d /run/log/journal ] || [ -L /run/log/journal ]; } && [ -s "${STATE_MOUNTPOINT}"/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)

View File

@ -79,3 +79,23 @@ 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"
# Set state mountpoint
STATE_MOUNTPOINT="/mnt/state"
setup_then_mount "state" "${STATE_MOUNTPOINT}"
export STATE_MOUNTPOINT
# Set data mountpoint
DATA_MOUNTPOINT="/mnt/data"
setup_then_mount "data" "${DATA_MOUNTPOINT}"
export DATA_MOUNTPOINT
# Mount the Supervisor database directory to a more accessible & backwards compatible location.
# TODO: DB should be moved to a managed volume and mounted to /data in-container.
# Handle the case of such a Supervisor volume already existing.
# NOTE: After this PR, it should be good to remove the OS's /data/database.sqlite mount.
if [ ! -f /data/database.sqlite ]; then
mkdir -p "${DATA_MOUNTPOINT}/resin-data/balena-supervisor"
mount -o bind,shared "${DATA_MOUNTPOINT}"/resin-data/balena-supervisor /data
fi
export DATABASE_PATH="/data/database.sqlite"

View File

@ -4,7 +4,7 @@ import { VolumeInspectInfo } from 'dockerode';
import { isNotFoundError, InternalInconsistencyError } from '../lib/errors';
import { safeRename } from '../lib/fs-utils';
import { pathOnRoot } from '../lib/host-utils';
import { pathOnData } from '../lib/host-utils';
import { docker } from '../lib/docker-utils';
import * as LogTypes from '../lib/log-types';
import log from '../lib/supervisor-console';
@ -97,8 +97,8 @@ export async function createFromPath(
.getVolume(Volume.generateDockerName(volume.appId, volume.name))
.inspect();
const volumePath = pathOnRoot(
path.join('mnt/data', ...inspect.Mountpoint.split(path.sep).slice(3)),
const volumePath = pathOnData(
path.join(...inspect.Mountpoint.split(path.sep).slice(3)),
);
await safeRename(oldPath, volumePath);

View File

@ -7,6 +7,9 @@ const supervisorNetworkInterface = 'supervisor0';
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 stateMountPoint =
checkString(process.env.STATE_MOUNTPOINT) || '/mnt/state';
const dataMountPoint = checkString(process.env.DATA_MOUNTPOINT) || '/mnt/data';
const constants = {
// Root overlay paths
@ -23,13 +26,13 @@ const constants = {
checkString(process.env.HOST_OS_VERSION_PATH) ||
withRootMount('/etc/os-release'),
// Data paths
dataMountPoint,
databasePath:
checkString(process.env.DATABASE_PATH) || '/data/database.sqlite',
appsJsonPath:
process.env.APPS_JSON_PATH || withRootMount('/mnt/data/apps.json'),
migrationBackupFile: 'backup.tgz',
appsJsonPath: path.join(dataMountPoint, 'apps.json'),
migrationBackupFile: path.join(dataMountPoint, 'backup.tgz'),
// State paths
stateMountPoint: '/mnt/state',
stateMountPoint,
// Other constants: network, Engine, /sys
containerId: checkString(process.env.SUPERVISOR_CONTAINER_ID) || undefined,
dockerSocket: process.env.DOCKER_SOCKET || '/var/run/docker.sock',

View File

@ -30,6 +30,17 @@ export const pathExistsOnRoot = async (p: string) =>
// This path is accessible from within the Supervisor container
export const pathOnBoot = withBase(constants.bootMountPoint);
// Returns an absolute path starting from the hostOS data partition
// This path is accessible from within the Supervisor container
export const pathOnData = withBase(constants.dataMountPoint);
// Returns an absolute path starting from the hostOS state partition
// This path is accessible from within the Supervisor container
export const pathOnState = withBase(constants.stateMountPoint);
export const pathExistsOnState = async (p: string) =>
await exists(pathOnState(p));
class CodedError extends Error {
constructor(msg: string, readonly code: number | string) {
super(msg);

View File

@ -15,7 +15,7 @@ import {
} from './errors';
import { docker } from './docker-utils';
import { log } from './supervisor-console';
import { pathOnRoot } from './host-utils';
import { pathOnData } from './host-utils';
import Volume from '../compose/volume';
import * as logger from '../logger';
import type {
@ -35,9 +35,7 @@ async function createVolumeFromLegacyData(
appUuid: string,
): Promise<Volume | void> {
const name = defaultLegacyVolume();
const legacyPath = pathOnRoot(
path.join('mnt/data/resin-data', appId.toString()),
);
const legacyPath = pathOnData(path.join('resin-data', appId.toString()));
try {
return await volumeManager.createFromPath(

View File

@ -10,17 +10,14 @@ 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';
import { pathOnData } from './host-utils';
export async function loadBackupFromMigration(
targetState: TargetState,
retryDelay: number,
): Promise<void> {
try {
const migrationExists = await exists(
pathOnRoot(path.join('mnt/data', constants.migrationBackupFile)),
);
if (!migrationExists) {
if (!(await exists(constants.migrationBackupFile))) {
return;
}
log.info('Migration backup detected');
@ -39,12 +36,12 @@ export async function loadBackupFromMigration(
const volumes = release?.volumes ?? {};
const backupPath = pathOnRoot('mnt/data/backup');
const backupPath = pathOnData('backup');
// We clear this path in case it exists from an incomplete run of this function
await unlinkAll(backupPath);
await mkdirp(backupPath);
await exec(`tar -xzf backup.tgz -C ${backupPath}`, {
cwd: pathOnRoot('mnt/data'),
cwd: pathOnData(),
});
for (const volumeName of await fs.readdir(backupPath)) {
@ -84,9 +81,7 @@ export async function loadBackupFromMigration(
}
await unlinkAll(backupPath);
await unlinkAll(
pathOnRoot(path.join('mnt/data', constants.migrationBackupFile)),
);
await unlinkAll(constants.migrationBackupFile);
} catch (err) {
log.error(`Error restoring migration backup, retrying: ${err}`);

View File

@ -27,7 +27,7 @@ export async function getStorageInfo(): Promise<{
let total = 0;
// First we find the block device which the data partition is part of
for (const partition of fsInfo) {
if (partition.mount === '/data') {
if (new RegExp('/data').test(partition.mount)) {
mainFs = partition.fs;
total = partition.size;
break;

View File

@ -5,14 +5,12 @@ import * as path from 'path';
import * as Lock from 'rwlock';
import { isRight } from 'fp-ts/lib/Either';
import * as constants from './constants';
import {
ENOENT,
UpdatesLockedError,
InternalInconsistencyError,
} from './errors';
import { exists } from './fs-utils';
import { pathOnRoot } from './host-utils';
import { pathOnRoot, pathExistsOnState } from './host-utils';
import * as config from '../config';
import * as lockfile from './lockfile';
import { NumericIdentifier } from '../types';
@ -48,8 +46,7 @@ export function abortIfHUPInProgress({
}): Promise<boolean | never> {
return Promise.all(
['rollback-health-breadcrumb', 'rollback-altboot-breadcrumb'].map(
(filename) =>
exists(pathOnRoot(path.join(constants.stateMountPoint, filename))),
(filename) => pathExistsOnState(filename),
),
).then((existsArray) => {
const anyExists = existsArray.some((e) => e);

View File

@ -7,7 +7,7 @@ import * as updateLock from '~/lib/update-lock';
import { UpdatesLockedError } from '~/lib/errors';
import * as config from '~/src/config';
import * as lockfile from '~/lib/lockfile';
import { pathOnRoot } from '~/lib/host-utils';
import { pathOnRoot, pathOnState } from '~/lib/host-utils';
describe('lib/update-lock', () => {
describe('abortIfHUPInProgress', () => {
@ -16,7 +16,7 @@ describe('lib/update-lock', () => {
'rollback-altboot-breadcrumb',
];
const breadcrumbsDir = pathOnRoot('/mnt/state');
const breadcrumbsDir = pathOnState();
const createBreadcrumb = (breadcrumb: string) =>
testfs({