From 9c6e5ee11f5014fd27c084e23b8fc2c8db7c3727 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne Date: Fri, 10 Dec 2021 15:15:59 +0000 Subject: [PATCH] Remove apps.json after initial preload This avoids the supervisor trying to get back to the preloaded target state if the database is deleted by any reason. It does this by moving the used apps.json to a backup location. Change-type: patch Depends-on: #1841 --- src/device-state/preload.ts | 34 +++++++++++++++++++++++++--------- src/lib/constants.ts | 5 ++++- test/05-device-state.spec.ts | 22 ++++++++++++++++++---- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/device-state/preload.ts b/src/device-state/preload.ts index 0e0161de..f87eb870 100644 --- a/src/device-state/preload.ts +++ b/src/device-state/preload.ts @@ -8,21 +8,19 @@ import * as deviceConfig from '../device-config'; import * as eventTracker from '../event-tracker'; import * as images from '../compose/images'; -import constants = require('../lib/constants'); import { AppsJsonParseError, EISDIR, ENOENT } from '../lib/errors'; import log from '../lib/supervisor-console'; import { convertLegacyAppsJson } from '../lib/migration'; import { AppsJsonFormat } from '../types/state'; +import * as fsUtils from '../lib/fs-utils'; -export async function loadTargetFromFile( - appsPath: Nullable, -): Promise { +export function appsJsonBackup(appsPath: string) { + return `${appsPath}.preloaded`; +} + +export async function loadTargetFromFile(appsPath: string): Promise { log.info('Attempting to load any preloaded applications'); - if (!appsPath) { - appsPath = constants.appsJsonPath; - } - try { const content = await fs.readFile(appsPath, 'utf8'); @@ -73,7 +71,7 @@ export async function loadTargetFromFile( } for (const image of imgs) { - const name = await images.normalise(image.name); + const name = images.normalise(image.name); image.name = name; await images.save(image); } @@ -114,5 +112,23 @@ export async function loadTargetFromFile( error: e, }); } + } finally { + const targetPath = appsJsonBackup(appsPath); + if (!(await fsUtils.exists(targetPath))) { + // Try to rename the path so the preload target state won't + // be used again if the database gets deleted for any reason. + // If the target file already exists or something fails, just debug + // the failure. + await fsUtils + .safeRename(appsPath, targetPath) + .then(() => fsUtils.writeFileAtomic(appsPath, '{}')) + .then(() => log.debug(`Migrated existing apps.json`)) + .catch((e) => + log.debug( + `Continuing without migrating apps.json because of`, + e.message, + ), + ); + } } } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e540c2e7..6e7a051b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import { checkString } from './validation'; const bootMountPointFromEnv = checkString(process.env.BOOT_MOUNTPOINT); @@ -49,7 +50,9 @@ const constants = { 'lo', supervisorNetworkInterface, ], - appsJsonPath: process.env.APPS_JSON_PATH || '/boot/apps.json', + appsJsonPath: + process.env.APPS_JSON_PATH || + path.join(rootMountPoint, '/mnt/data', 'apps.json'), ipAddressUpdateInterval: 30 * 1000, imageCleanupErrorIgnoreTimeout: 3600 * 1000, maxDeltaDownloads: 3, diff --git a/test/05-device-state.spec.ts b/test/05-device-state.spec.ts index a4e6470d..aed71265 100644 --- a/test/05-device-state.spec.ts +++ b/test/05-device-state.spec.ts @@ -10,10 +10,14 @@ import * as images from '../src/compose/images'; import { ConfigTxt } from '../src/config/backends/config-txt'; import * as deviceState from '../src/device-state'; import * as deviceConfig from '../src/device-config'; -import { loadTargetFromFile } from '../src/device-state/preload'; +import { + loadTargetFromFile, + appsJsonBackup, +} from '../src/device-state/preload'; import Service from '../src/compose/service'; import { intialiseContractRequirements } from '../src/lib/contracts'; import * as updateLock from '../src/lib/update-lock'; +import * as fsUtils from '../src/lib/fs-utils'; const mockedInitialConfig = { RESIN_SUPERVISOR_CONNECTIVITY_CHECK: 'true', @@ -224,8 +228,10 @@ describe('deviceState', () => { }); it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => { - await loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json'); + const appsJson = process.env.ROOT_MOUNTPOINT + '/apps.json'; + await loadTargetFromFile(appsJson); const targetState = await deviceState.getTarget(); + expect(await fsUtils.exists(appsJsonBackup(appsJson))).to.be.true; expect(targetState) .to.have.property('local') @@ -283,14 +289,22 @@ describe('deviceState', () => { .to.have.property('labels') .that.has.property('io.balena.something') .that.equals('bar'); + + // Restore renamed apps.json + await fsUtils.safeRename(appsJsonBackup(appsJson), appsJson); }); it('stores info for pinning a device after loading an apps.json with a pinDevice field', async () => { - await loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps-pin.json'); + const appsJson = process.env.ROOT_MOUNTPOINT + '/apps-pin.json'; + await loadTargetFromFile(appsJson); const pinned = await config.get('pinDevice'); expect(pinned).to.have.property('app').that.equals(1234); expect(pinned).to.have.property('commit').that.equals('abcdef'); + expect(await fsUtils.exists(appsJsonBackup(appsJson))).to.be.true; + + // Restore renamed apps.json + await fsUtils.safeRename(appsJsonBackup(appsJson), appsJson); }); it('emits a change event when a new state is reported', (done) => { @@ -303,7 +317,7 @@ describe('deviceState', () => { const services: Service[] = []; for (const service of testTarget.local.apps['1234'].services) { - const imageName = await images.normalise(service.image); + const imageName = images.normalise(service.image); service.image = imageName; (service as any).imageName = imageName; services.push(