mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-21 18:06:47 +00:00
Simplify lock interface to prep for adding takeLock to state funnel
This commit changes a few things: * Pass `force` to `takeLock` step directly. This allows us to remove the `lockFn` used by app manager's action executors, setting takeLock as the main interface to interact with the update lock module. Note that this commit by itself will not pass tests, as no update locking occurs where it once did. This will be amended in the next commit. * Remove locking functions from doRestart & doPurge, as this is the only area where skipLock is required. * Remove `skipLock` interface, as it's redundant with the functionality of `force`. The only time `skipLock` is true is in doRestart/doPurge, as those API methods are already run within a lock function. We removed the lock function which removes the need for skipLock, and in the next commit we'll add locking as a composition step to replace the functionality removed here. * Remove some methods not in use, such as app manager's `stopAll`. Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
parent
2f728ee43e
commit
cf8d8cedd7
@ -45,6 +45,7 @@ export interface UpdateState {
|
|||||||
containerIds: Dictionary<string>;
|
containerIds: Dictionary<string>;
|
||||||
downloading: string[];
|
downloading: string[];
|
||||||
locksTaken: LocksTakenMap;
|
locksTaken: LocksTakenMap;
|
||||||
|
force: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChangingPair<T> {
|
interface ChangingPair<T> {
|
||||||
@ -111,8 +112,15 @@ export class App {
|
|||||||
// Check to see if we need to polyfill in some "new" data for legacy services
|
// Check to see if we need to polyfill in some "new" data for legacy services
|
||||||
this.migrateLegacy(target);
|
this.migrateLegacy(target);
|
||||||
|
|
||||||
// Check for changes in the volumes. We don't remove any volumes until we remove an
|
let steps: CompositionStep[] = [];
|
||||||
// entire app
|
|
||||||
|
// Any services which have died get a remove step
|
||||||
|
for (const service of this.services) {
|
||||||
|
if (service.status === 'Dead') {
|
||||||
|
steps.push(generateStep('remove', { current: service }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const volumeChanges = this.compareComponents(
|
const volumeChanges = this.compareComponents(
|
||||||
this.volumes,
|
this.volumes,
|
||||||
target.volumes,
|
target.volumes,
|
||||||
@ -124,15 +132,6 @@ export class App {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let steps: CompositionStep[] = [];
|
|
||||||
|
|
||||||
// Any services which have died get a remove step
|
|
||||||
for (const service of this.services) {
|
|
||||||
if (service.status === 'Dead') {
|
|
||||||
steps.push(generateStep('remove', { current: service }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { removePairs, installPairs, updatePairs } = this.compareServices(
|
const { removePairs, installPairs, updatePairs } = this.compareServices(
|
||||||
this.services,
|
this.services,
|
||||||
target.services,
|
target.services,
|
||||||
@ -571,7 +570,12 @@ export class App {
|
|||||||
current.isEqualConfig(target, context.containerIds)
|
current.isEqualConfig(target, context.containerIds)
|
||||||
) {
|
) {
|
||||||
// Update service metadata or start/stop a service
|
// Update service metadata or start/stop a service
|
||||||
return this.generateContainerStep(current, target, context.locksTaken);
|
return this.generateContainerStep(
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
context.locksTaken,
|
||||||
|
context.force,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let strategy: string;
|
let strategy: string;
|
||||||
@ -664,6 +668,7 @@ export class App {
|
|||||||
current: Service,
|
current: Service,
|
||||||
target: Service,
|
target: Service,
|
||||||
locksTaken: LocksTakenMap,
|
locksTaken: LocksTakenMap,
|
||||||
|
force: boolean,
|
||||||
) {
|
) {
|
||||||
// Update container metadata if service release has changed
|
// Update container metadata if service release has changed
|
||||||
if (current.commit !== target.commit) {
|
if (current.commit !== target.commit) {
|
||||||
@ -679,6 +684,7 @@ export class App {
|
|||||||
return generateStep('takeLock', {
|
return generateStep('takeLock', {
|
||||||
appId: target.appId,
|
appId: target.appId,
|
||||||
services: [target.serviceName],
|
services: [target.serviceName],
|
||||||
|
force,
|
||||||
});
|
});
|
||||||
} else if (target.config.running !== current.config.running) {
|
} else if (target.config.running !== current.config.running) {
|
||||||
if (target.config.running) {
|
if (target.config.running) {
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
ContractViolationError,
|
ContractViolationError,
|
||||||
InternalInconsistencyError,
|
InternalInconsistencyError,
|
||||||
} from '../lib/errors';
|
} from '../lib/errors';
|
||||||
import { getServicesLockedByAppId, lock } from '../lib/update-lock';
|
import { getServicesLockedByAppId } from '../lib/update-lock';
|
||||||
import { checkTruthy } from '../lib/validation';
|
import { checkTruthy } from '../lib/validation';
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
@ -64,7 +64,6 @@ export function resetTimeSpentFetching(value: number = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actionExecutors = getExecutors({
|
const actionExecutors = getExecutors({
|
||||||
lockFn: lock,
|
|
||||||
callbacks: {
|
callbacks: {
|
||||||
fetchStart: () => {
|
fetchStart: () => {
|
||||||
fetchesInProgress += 1;
|
fetchesInProgress += 1;
|
||||||
@ -120,6 +119,7 @@ export async function getRequiredSteps(
|
|||||||
targetApps: InstancedAppState,
|
targetApps: InstancedAppState,
|
||||||
keepImages?: boolean,
|
keepImages?: boolean,
|
||||||
keepVolumes?: boolean,
|
keepVolumes?: boolean,
|
||||||
|
force: boolean = false,
|
||||||
): Promise<CompositionStep[]> {
|
): Promise<CompositionStep[]> {
|
||||||
// get some required data
|
// get some required data
|
||||||
const [downloading, availableImages, { localMode, delta }] =
|
const [downloading, availableImages, { localMode, delta }] =
|
||||||
@ -146,6 +146,7 @@ export async function getRequiredSteps(
|
|||||||
// Volumes are not removed when stopping an app when going to local mode
|
// Volumes are not removed when stopping an app when going to local mode
|
||||||
keepVolumes,
|
keepVolumes,
|
||||||
delta,
|
delta,
|
||||||
|
force,
|
||||||
downloading,
|
downloading,
|
||||||
availableImages,
|
availableImages,
|
||||||
containerIdsByAppId,
|
containerIdsByAppId,
|
||||||
@ -161,6 +162,7 @@ export async function inferNextSteps(
|
|||||||
keepImages = false,
|
keepImages = false,
|
||||||
keepVolumes = false,
|
keepVolumes = false,
|
||||||
delta = true,
|
delta = true,
|
||||||
|
force = false,
|
||||||
downloading = [] as UpdateState['downloading'],
|
downloading = [] as UpdateState['downloading'],
|
||||||
availableImages = [] as UpdateState['availableImages'],
|
availableImages = [] as UpdateState['availableImages'],
|
||||||
containerIdsByAppId = {} as {
|
containerIdsByAppId = {} as {
|
||||||
@ -219,6 +221,7 @@ export async function inferNextSteps(
|
|||||||
containerIds: containerIdsByAppId[id],
|
containerIds: containerIdsByAppId[id],
|
||||||
downloading,
|
downloading,
|
||||||
locksTaken,
|
locksTaken,
|
||||||
|
force,
|
||||||
},
|
},
|
||||||
targetApps[id],
|
targetApps[id],
|
||||||
),
|
),
|
||||||
@ -233,6 +236,7 @@ export async function inferNextSteps(
|
|||||||
downloading,
|
downloading,
|
||||||
containerIds: containerIdsByAppId[id],
|
containerIds: containerIdsByAppId[id],
|
||||||
locksTaken,
|
locksTaken,
|
||||||
|
force,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -257,6 +261,7 @@ export async function inferNextSteps(
|
|||||||
containerIds: containerIdsByAppId[id] ?? {},
|
containerIds: containerIdsByAppId[id] ?? {},
|
||||||
downloading,
|
downloading,
|
||||||
locksTaken,
|
locksTaken,
|
||||||
|
force,
|
||||||
},
|
},
|
||||||
targetApps[id],
|
targetApps[id],
|
||||||
),
|
),
|
||||||
@ -301,17 +306,6 @@ export async function inferNextSteps(
|
|||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopAll({ force = false, skipLock = false } = {}) {
|
|
||||||
const services = await serviceManager.getAll();
|
|
||||||
await Promise.all(
|
|
||||||
services.map(async (s) => {
|
|
||||||
return lock(s.appId, { force, skipLock }, async () => {
|
|
||||||
await serviceManager.kill(s, { removeContainer: false, wait: true });
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following two function may look pretty odd, but after the move to uuids,
|
// The following two function may look pretty odd, but after the move to uuids,
|
||||||
// there's a chance that the current running apps don't have a uuid set. We
|
// there's a chance that the current running apps don't have a uuid set. We
|
||||||
// still need to be able to work on these and perform various state changes. To
|
// still need to be able to work on these and perform various state changes. To
|
||||||
@ -501,7 +495,7 @@ function killServicesUsingApi(current: InstancedAppState): CompositionStep[] {
|
|||||||
// intermediate targets to perform changes
|
// intermediate targets to perform changes
|
||||||
export async function executeStep(
|
export async function executeStep(
|
||||||
step: CompositionStep,
|
step: CompositionStep,
|
||||||
{ force = false, skipLock = false } = {},
|
{ force = false } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!validActions.includes(step.action)) {
|
if (!validActions.includes(step.action)) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
@ -515,7 +509,6 @@ export async function executeStep(
|
|||||||
await actionExecutors[step.action]({
|
await actionExecutors[step.action]({
|
||||||
...step,
|
...step,
|
||||||
force,
|
force,
|
||||||
skipLock,
|
|
||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import type { Image } from './images';
|
import type { Image } from './images';
|
||||||
import * as images from './images';
|
import * as images from './images';
|
||||||
@ -13,33 +11,22 @@ import * as commitStore from './commit';
|
|||||||
import * as updateLock from '../lib/update-lock';
|
import * as updateLock from '../lib/update-lock';
|
||||||
import type { DeviceLegacyReport } from '../types/state';
|
import type { DeviceLegacyReport } from '../types/state';
|
||||||
|
|
||||||
interface BaseCompositionStepArgs {
|
|
||||||
force?: boolean;
|
|
||||||
skipLock?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Most of the steps take the
|
|
||||||
// BaseCompositionStepArgs, but some also take an options
|
|
||||||
// structure which includes some of the same fields. It
|
|
||||||
// would be nice to remove the need for this
|
|
||||||
interface CompositionStepArgs {
|
interface CompositionStepArgs {
|
||||||
stop: {
|
stop: {
|
||||||
current: Service;
|
current: Service;
|
||||||
options?: {
|
options?: {
|
||||||
skipLock?: boolean;
|
|
||||||
wait?: boolean;
|
wait?: boolean;
|
||||||
};
|
};
|
||||||
} & BaseCompositionStepArgs;
|
};
|
||||||
kill: {
|
kill: {
|
||||||
current: Service;
|
current: Service;
|
||||||
options?: {
|
options?: {
|
||||||
skipLock?: boolean;
|
|
||||||
wait?: boolean;
|
wait?: boolean;
|
||||||
};
|
};
|
||||||
} & BaseCompositionStepArgs;
|
};
|
||||||
remove: {
|
remove: {
|
||||||
current: Service;
|
current: Service;
|
||||||
} & BaseCompositionStepArgs;
|
};
|
||||||
updateMetadata: {
|
updateMetadata: {
|
||||||
current: Service;
|
current: Service;
|
||||||
target: Service;
|
target: Service;
|
||||||
@ -47,13 +34,10 @@ interface CompositionStepArgs {
|
|||||||
restart: {
|
restart: {
|
||||||
current: Service;
|
current: Service;
|
||||||
target: Service;
|
target: Service;
|
||||||
options?: {
|
};
|
||||||
skipLock?: boolean;
|
|
||||||
};
|
|
||||||
} & BaseCompositionStepArgs;
|
|
||||||
start: {
|
start: {
|
||||||
target: Service;
|
target: Service;
|
||||||
} & BaseCompositionStepArgs;
|
};
|
||||||
updateCommit: {
|
updateCommit: {
|
||||||
target: string;
|
target: string;
|
||||||
appId: number;
|
appId: number;
|
||||||
@ -62,10 +46,9 @@ interface CompositionStepArgs {
|
|||||||
current: Service;
|
current: Service;
|
||||||
target: Service;
|
target: Service;
|
||||||
options?: {
|
options?: {
|
||||||
skipLock?: boolean;
|
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
};
|
};
|
||||||
} & BaseCompositionStepArgs;
|
};
|
||||||
fetch: {
|
fetch: {
|
||||||
image: Image;
|
image: Image;
|
||||||
serviceName: string;
|
serviceName: string;
|
||||||
@ -94,6 +77,7 @@ interface CompositionStepArgs {
|
|||||||
takeLock: {
|
takeLock: {
|
||||||
appId: number;
|
appId: number;
|
||||||
services: string[];
|
services: string[];
|
||||||
|
force: boolean;
|
||||||
};
|
};
|
||||||
releaseLock: {
|
releaseLock: {
|
||||||
appId: number;
|
appId: number;
|
||||||
@ -119,13 +103,6 @@ export function generateStep<T extends CompositionStepAction>(
|
|||||||
type Executors<T extends CompositionStepAction> = {
|
type Executors<T extends CompositionStepAction> = {
|
||||||
[key in T]: (step: CompositionStepT<key>) => Promise<unknown>;
|
[key in T]: (step: CompositionStepT<key>) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
type LockingFn = (
|
|
||||||
// TODO: Once the entire codebase is typescript, change
|
|
||||||
// this to number
|
|
||||||
app: number | number[] | null,
|
|
||||||
args: BaseCompositionStepArgs,
|
|
||||||
fn: () => Promise<unknown>,
|
|
||||||
) => Promise<unknown>;
|
|
||||||
|
|
||||||
interface CompositionCallbacks {
|
interface CompositionCallbacks {
|
||||||
// TODO: Once the entire codebase is typescript, change
|
// TODO: Once the entire codebase is typescript, change
|
||||||
@ -137,38 +114,20 @@ interface CompositionCallbacks {
|
|||||||
bestDeltaSource: (image: Image, available: Image[]) => string | null;
|
bestDeltaSource: (image: Image, available: Image[]) => string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExecutors(app: {
|
export function getExecutors(app: { callbacks: CompositionCallbacks }) {
|
||||||
lockFn: LockingFn;
|
|
||||||
callbacks: CompositionCallbacks;
|
|
||||||
}) {
|
|
||||||
const executors: Executors<CompositionStepAction> = {
|
const executors: Executors<CompositionStepAction> = {
|
||||||
stop: (step) => {
|
stop: async (step) => {
|
||||||
return app.lockFn(
|
// Should always be preceded by a takeLock step,
|
||||||
step.current.appId,
|
// so the call is executed assuming that the lock is taken.
|
||||||
{
|
await serviceManager.kill(step.current, {
|
||||||
force: step.force,
|
removeContainer: false,
|
||||||
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
|
wait: step.options?.wait || false,
|
||||||
},
|
});
|
||||||
async () => {
|
|
||||||
const wait = _.get(step, ['options', 'wait'], false);
|
|
||||||
await serviceManager.kill(step.current, {
|
|
||||||
removeContainer: false,
|
|
||||||
wait,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
kill: (step) => {
|
kill: async (step) => {
|
||||||
return app.lockFn(
|
// Should always be preceded by a takeLock step,
|
||||||
step.current.appId,
|
// so the call is executed assuming that the lock is taken.
|
||||||
{
|
await serviceManager.kill(step.current);
|
||||||
force: step.force,
|
|
||||||
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
await serviceManager.kill(step.current);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
remove: async (step) => {
|
remove: async (step) => {
|
||||||
// Only called for dead containers, so no need to
|
// Only called for dead containers, so no need to
|
||||||
@ -180,18 +139,11 @@ export function getExecutors(app: {
|
|||||||
// so the call is executed assuming that the lock is taken.
|
// so the call is executed assuming that the lock is taken.
|
||||||
await serviceManager.updateMetadata(step.current, step.target);
|
await serviceManager.updateMetadata(step.current, step.target);
|
||||||
},
|
},
|
||||||
restart: (step) => {
|
restart: async (step) => {
|
||||||
return app.lockFn(
|
// Should always be preceded by a takeLock step,
|
||||||
step.current.appId,
|
// so the call is executed assuming that the lock is taken.
|
||||||
{
|
await serviceManager.kill(step.current, { wait: true });
|
||||||
force: step.force,
|
await serviceManager.start(step.target);
|
||||||
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
await serviceManager.kill(step.current, { wait: true });
|
|
||||||
await serviceManager.start(step.target);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
start: async (step) => {
|
start: async (step) => {
|
||||||
await serviceManager.start(step.target);
|
await serviceManager.start(step.target);
|
||||||
@ -199,17 +151,10 @@ export function getExecutors(app: {
|
|||||||
updateCommit: async (step) => {
|
updateCommit: async (step) => {
|
||||||
await commitStore.upsertCommitForApp(step.appId, step.target);
|
await commitStore.upsertCommitForApp(step.appId, step.target);
|
||||||
},
|
},
|
||||||
handover: (step) => {
|
handover: async (step) => {
|
||||||
return app.lockFn(
|
// Should always be preceded by a takeLock step,
|
||||||
step.current.appId,
|
// so the call is executed assuming that the lock is taken.
|
||||||
{
|
await serviceManager.handover(step.current, step.target);
|
||||||
force: step.force,
|
|
||||||
skipLock: step.skipLock || _.get(step, ['options', 'skipLock']),
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
await serviceManager.handover(step.current, step.target);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
fetch: async (step) => {
|
fetch: async (step) => {
|
||||||
const startTime = process.hrtime();
|
const startTime = process.hrtime();
|
||||||
@ -271,7 +216,7 @@ export function getExecutors(app: {
|
|||||||
/* async noop */
|
/* async noop */
|
||||||
},
|
},
|
||||||
takeLock: async (step) => {
|
takeLock: async (step) => {
|
||||||
await updateLock.takeLock(step.appId, step.services);
|
await updateLock.takeLock(step.appId, step.services, step.force);
|
||||||
},
|
},
|
||||||
releaseLock: async (step) => {
|
releaseLock: async (step) => {
|
||||||
await updateLock.releaseLock(step.appId);
|
await updateLock.releaseLock(step.appId);
|
||||||
|
@ -11,11 +11,11 @@ import * as applicationManager from '../compose/application-manager';
|
|||||||
import type { CompositionStepAction } from '../compose/composition-steps';
|
import type { CompositionStepAction } from '../compose/composition-steps';
|
||||||
import { generateStep } from '../compose/composition-steps';
|
import { generateStep } from '../compose/composition-steps';
|
||||||
import * as commitStore from '../compose/commit';
|
import * as commitStore from '../compose/commit';
|
||||||
|
import type Service from '../compose/service';
|
||||||
import { getApp } from '../device-state/db-format';
|
import { getApp } from '../device-state/db-format';
|
||||||
import * as TargetState from '../device-state/target-state';
|
import * as TargetState from '../device-state/target-state';
|
||||||
import log from '../lib/supervisor-console';
|
import log from '../lib/supervisor-console';
|
||||||
import blink = require('../lib/blink');
|
import blink = require('../lib/blink');
|
||||||
import { lock } from '../lib/update-lock';
|
|
||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
import {
|
import {
|
||||||
InternalInconsistencyError,
|
InternalInconsistencyError,
|
||||||
@ -88,32 +88,29 @@ export const regenerateKey = async (oldKey: string) => {
|
|||||||
export const doRestart = async (appId: number, force: boolean = false) => {
|
export const doRestart = async (appId: number, force: boolean = false) => {
|
||||||
await deviceState.initialized();
|
await deviceState.initialized();
|
||||||
|
|
||||||
return await lock(appId, { force }, async () => {
|
const currentState = await deviceState.getCurrentState();
|
||||||
const currentState = await deviceState.getCurrentState();
|
if (currentState.local.apps?.[appId] == null) {
|
||||||
if (currentState.local.apps?.[appId] == null) {
|
throw new InternalInconsistencyError(
|
||||||
throw new InternalInconsistencyError(
|
`Application with ID ${appId} is not in the current state`,
|
||||||
`Application with ID ${appId} is not in the current state`,
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
const app = currentState.local.apps[appId];
|
|
||||||
const services = app.services;
|
|
||||||
app.services = [];
|
|
||||||
|
|
||||||
return deviceState
|
const app = currentState.local.apps[appId];
|
||||||
.applyIntermediateTarget(currentState, {
|
const services = app.services;
|
||||||
skipLock: true,
|
|
||||||
})
|
try {
|
||||||
.then(() => {
|
// Set target so that services get deleted
|
||||||
app.services = services;
|
app.services = [];
|
||||||
return deviceState.applyIntermediateTarget(currentState, {
|
await deviceState.applyIntermediateTarget(currentState, { force });
|
||||||
skipLock: true,
|
// Restore services
|
||||||
keepVolumes: false,
|
app.services = services;
|
||||||
});
|
return deviceState.applyIntermediateTarget(currentState, {
|
||||||
})
|
keepVolumes: false,
|
||||||
.finally(() => {
|
force,
|
||||||
deviceState.triggerApplyTarget();
|
});
|
||||||
});
|
} finally {
|
||||||
});
|
deviceState.triggerApplyTarget();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,47 +128,37 @@ export const doPurge = async (appId: number, force: boolean = false) => {
|
|||||||
'Purge data',
|
'Purge data',
|
||||||
);
|
);
|
||||||
|
|
||||||
return await lock(appId, { force }, async () => {
|
const currentState = await deviceState.getCurrentState();
|
||||||
const currentState = await deviceState.getCurrentState();
|
if (currentState.local.apps?.[appId] == null) {
|
||||||
if (currentState.local.apps?.[appId] == null) {
|
throw new InternalInconsistencyError(
|
||||||
throw new InternalInconsistencyError(
|
`Application with ID ${appId} is not in the current state`,
|
||||||
`Application with ID ${appId} is not in the current state`,
|
);
|
||||||
);
|
}
|
||||||
}
|
// Save & delete the app from the current state
|
||||||
|
const app = currentState.local.apps[appId];
|
||||||
|
delete currentState.local.apps[appId];
|
||||||
|
|
||||||
const app = currentState.local.apps[appId];
|
try {
|
||||||
|
// Purposely tell the apply function to delete volumes so
|
||||||
// Delete the app from the current state
|
// they can get deleted even in local mode
|
||||||
delete currentState.local.apps[appId];
|
await deviceState.applyIntermediateTarget(currentState, {
|
||||||
|
keepVolumes: false,
|
||||||
return deviceState
|
force,
|
||||||
.applyIntermediateTarget(currentState, {
|
|
||||||
skipLock: true,
|
|
||||||
// Purposely tell the apply function to delete volumes so they can get
|
|
||||||
// deleted even in local mode
|
|
||||||
keepVolumes: false,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
currentState.local.apps[appId] = app;
|
|
||||||
return deviceState.applyIntermediateTarget(currentState, {
|
|
||||||
skipLock: true,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
deviceState.triggerApplyTarget();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() =>
|
|
||||||
logger.logSystemMessage('Purged data', { appId }, 'Purge data success'),
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
logger.logSystemMessage(
|
|
||||||
`Error purging data: ${err}`,
|
|
||||||
{ appId, error: err },
|
|
||||||
'Purge data error',
|
|
||||||
);
|
|
||||||
throw err;
|
|
||||||
});
|
});
|
||||||
|
// Restore user app after purge
|
||||||
|
currentState.local.apps[appId] = app;
|
||||||
|
await deviceState.applyIntermediateTarget(currentState);
|
||||||
|
logger.logSystemMessage('Purged data', { appId }, 'Purge data success');
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.logSystemMessage(
|
||||||
|
`Error purging data: ${err}`,
|
||||||
|
{ appId, error: err?.message ?? err },
|
||||||
|
'Purge data error',
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
deviceState.triggerApplyTarget();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type ClientError = BadRequestError | NotFoundError;
|
type ClientError = BadRequestError | NotFoundError;
|
||||||
@ -224,6 +211,57 @@ export const executeDeviceAction = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used internally by executeServiceAction to handle locks
|
||||||
|
* around execution of a service action.
|
||||||
|
*/
|
||||||
|
const executeDeviceActionWithLock = async ({
|
||||||
|
action,
|
||||||
|
appId,
|
||||||
|
currentService,
|
||||||
|
targetService,
|
||||||
|
force = false,
|
||||||
|
}: {
|
||||||
|
action: CompositionStepAction;
|
||||||
|
appId: number;
|
||||||
|
currentService?: Service;
|
||||||
|
targetService?: Service;
|
||||||
|
force: boolean;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
if (currentService) {
|
||||||
|
// Take lock for current service to be modified / stopped
|
||||||
|
await executeDeviceAction(
|
||||||
|
generateStep('takeLock', {
|
||||||
|
appId,
|
||||||
|
services: [currentService.serviceName],
|
||||||
|
force,
|
||||||
|
}),
|
||||||
|
// FIXME: deviceState.executeStepAction only accepts force as a separate arg
|
||||||
|
// instead of reading force from the step object, so we have to pass it twice
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute action on service
|
||||||
|
await executeDeviceAction(
|
||||||
|
generateStep(action, {
|
||||||
|
current: currentService,
|
||||||
|
target: targetService,
|
||||||
|
wait: true,
|
||||||
|
}),
|
||||||
|
force,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// Release lock regardless of action success to prevent leftover lockfile
|
||||||
|
await executeDeviceAction(
|
||||||
|
generateStep('releaseLock', {
|
||||||
|
appId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a composition step action on a service.
|
* Executes a composition step action on a service.
|
||||||
* isLegacy indicates that the action is being called from a legacy (v1) endpoint,
|
* isLegacy indicates that the action is being called from a legacy (v1) endpoint,
|
||||||
@ -279,15 +317,23 @@ export const executeServiceAction = async ({
|
|||||||
throw new NotFoundError(messages.targetServiceNotFound);
|
throw new NotFoundError(messages.targetServiceNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute action on service
|
// A single service start action doesn't require locks
|
||||||
return await executeDeviceAction(
|
if (action === 'start') {
|
||||||
generateStep(action, {
|
// Execute action on service
|
||||||
current: currentService,
|
await executeDeviceAction(
|
||||||
target: targetService,
|
generateStep(action, {
|
||||||
wait: true,
|
target: targetService,
|
||||||
}),
|
}),
|
||||||
force,
|
);
|
||||||
);
|
} else {
|
||||||
|
await executeDeviceActionWithLock({
|
||||||
|
action,
|
||||||
|
appId,
|
||||||
|
currentService,
|
||||||
|
targetService,
|
||||||
|
force,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -574,11 +574,7 @@ export async function shutdown({
|
|||||||
// should happen via intermediate targets
|
// should happen via intermediate targets
|
||||||
export async function executeStepAction(
|
export async function executeStepAction(
|
||||||
step: DeviceStateStep<PossibleStepTargets>,
|
step: DeviceStateStep<PossibleStepTargets>,
|
||||||
{
|
{ force, initial }: { force?: boolean; initial?: boolean },
|
||||||
force,
|
|
||||||
initial,
|
|
||||||
skipLock,
|
|
||||||
}: { force?: boolean; initial?: boolean; skipLock?: boolean },
|
|
||||||
) {
|
) {
|
||||||
if (deviceConfig.isValidAction(step.action)) {
|
if (deviceConfig.isValidAction(step.action)) {
|
||||||
await deviceConfig.executeStepAction(step as deviceConfig.ConfigStep, {
|
await deviceConfig.executeStepAction(step as deviceConfig.ConfigStep, {
|
||||||
@ -587,7 +583,6 @@ export async function executeStepAction(
|
|||||||
} else if (applicationManager.validActions.includes(step.action)) {
|
} else if (applicationManager.validActions.includes(step.action)) {
|
||||||
return applicationManager.executeStep(step as any, {
|
return applicationManager.executeStep(step as any, {
|
||||||
force,
|
force,
|
||||||
skipLock,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
switch (step.action) {
|
switch (step.action) {
|
||||||
@ -613,11 +608,9 @@ export async function applyStep(
|
|||||||
{
|
{
|
||||||
force,
|
force,
|
||||||
initial,
|
initial,
|
||||||
skipLock,
|
|
||||||
}: {
|
}: {
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
initial?: boolean;
|
initial?: boolean;
|
||||||
skipLock?: boolean;
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
@ -627,7 +620,6 @@ export async function applyStep(
|
|||||||
await executeStepAction(step, {
|
await executeStepAction(step, {
|
||||||
force,
|
force,
|
||||||
initial,
|
initial,
|
||||||
skipLock,
|
|
||||||
});
|
});
|
||||||
emitAsync('step-completed', null, step);
|
emitAsync('step-completed', null, step);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -685,7 +677,6 @@ export const applyTarget = async ({
|
|||||||
force = false,
|
force = false,
|
||||||
initial = false,
|
initial = false,
|
||||||
intermediate = false,
|
intermediate = false,
|
||||||
skipLock = false,
|
|
||||||
nextDelay = 200,
|
nextDelay = 200,
|
||||||
retryCount = 0,
|
retryCount = 0,
|
||||||
keepVolumes = undefined as boolean | undefined,
|
keepVolumes = undefined as boolean | undefined,
|
||||||
@ -724,6 +715,7 @@ export const applyTarget = async ({
|
|||||||
// the value
|
// the value
|
||||||
intermediate || undefined,
|
intermediate || undefined,
|
||||||
keepVolumes,
|
keepVolumes,
|
||||||
|
force,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_.isEmpty(appSteps)) {
|
if (_.isEmpty(appSteps)) {
|
||||||
@ -769,16 +761,13 @@ export const applyTarget = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(steps.map((s) => applyStep(s, { force, initial })));
|
||||||
steps.map((s) => applyStep(s, { force, initial, skipLock })),
|
|
||||||
);
|
|
||||||
|
|
||||||
await setTimeout(nextDelay);
|
await setTimeout(nextDelay);
|
||||||
await applyTarget({
|
await applyTarget({
|
||||||
force,
|
force,
|
||||||
initial,
|
initial,
|
||||||
intermediate,
|
intermediate,
|
||||||
skipLock,
|
|
||||||
nextDelay,
|
nextDelay,
|
||||||
retryCount,
|
retryCount,
|
||||||
keepVolumes,
|
keepVolumes,
|
||||||
@ -884,11 +873,7 @@ export function triggerApplyTarget({
|
|||||||
|
|
||||||
export async function applyIntermediateTarget(
|
export async function applyIntermediateTarget(
|
||||||
intermediate: InstancedDeviceState,
|
intermediate: InstancedDeviceState,
|
||||||
{
|
{ force = false, keepVolumes = undefined as boolean | undefined } = {},
|
||||||
force = false,
|
|
||||||
skipLock = false,
|
|
||||||
keepVolumes = undefined as boolean | undefined,
|
|
||||||
} = {},
|
|
||||||
) {
|
) {
|
||||||
return pausingApply(async () => {
|
return pausingApply(async () => {
|
||||||
// TODO: Make sure we don't accidentally overwrite this
|
// TODO: Make sure we don't accidentally overwrite this
|
||||||
@ -897,7 +882,6 @@ export async function applyIntermediateTarget(
|
|||||||
return applyTarget({
|
return applyTarget({
|
||||||
intermediate: true,
|
intermediate: true,
|
||||||
force,
|
force,
|
||||||
skipLock,
|
|
||||||
keepVolumes,
|
keepVolumes,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
intermediateTarget = null;
|
intermediateTarget = null;
|
||||||
|
@ -81,7 +81,11 @@ async function dispose(
|
|||||||
* Composition step used by Supervisor compose module.
|
* Composition step used by Supervisor compose module.
|
||||||
* Take all locks for an appId | appUuid, creating directories if they don't exist.
|
* Take all locks for an appId | appUuid, creating directories if they don't exist.
|
||||||
*/
|
*/
|
||||||
export async function takeLock(appId: number, services: string[]) {
|
export async function takeLock(
|
||||||
|
appId: number,
|
||||||
|
services: string[],
|
||||||
|
force: boolean = false,
|
||||||
|
) {
|
||||||
const release = await takeGlobalLockRW(appId);
|
const release = await takeGlobalLockRW(appId);
|
||||||
try {
|
try {
|
||||||
const actuallyLocked: string[] = [];
|
const actuallyLocked: string[] = [];
|
||||||
@ -93,7 +97,7 @@ export async function takeLock(appId: number, services: string[]) {
|
|||||||
);
|
);
|
||||||
for (const service of servicesWithoutLock) {
|
for (const service of servicesWithoutLock) {
|
||||||
await mkdirp(pathOnRoot(lockPath(appId, service)));
|
await mkdirp(pathOnRoot(lockPath(appId, service)));
|
||||||
await lockService(appId, service);
|
await lockService(appId, service, force);
|
||||||
actuallyLocked.push(service);
|
actuallyLocked.push(service);
|
||||||
}
|
}
|
||||||
return actuallyLocked;
|
return actuallyLocked;
|
||||||
@ -193,17 +197,14 @@ export function getServicesLockedByAppId(): LocksTakenMap {
|
|||||||
* all existing lockfiles before performing the operation
|
* all existing lockfiles before performing the operation
|
||||||
*
|
*
|
||||||
* TODO: convert to native Promises and async/await. May require native implementation of Bluebird's dispose / using
|
* TODO: convert to native Promises and async/await. May require native implementation of Bluebird's dispose / using
|
||||||
*
|
|
||||||
* TODO: Remove skipLock as it's not a good interface. If lock is called it should try to take the lock
|
|
||||||
* without an option to skip.
|
|
||||||
*/
|
*/
|
||||||
export async function lock<T>(
|
export async function lock<T>(
|
||||||
appId: number | number[],
|
appId: number | number[],
|
||||||
{ force = false, skipLock = false }: { force: boolean; skipLock?: boolean },
|
{ force = false }: { force: boolean },
|
||||||
fn: () => Resolvable<T>,
|
fn: () => Resolvable<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const appIdsToLock = Array.isArray(appId) ? appId : [appId];
|
const appIdsToLock = Array.isArray(appId) ? appId : [appId];
|
||||||
if (skipLock || !appId || !appIdsToLock.length) {
|
if (!appId || !appIdsToLock.length) {
|
||||||
return fn();
|
return fn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -996,7 +996,7 @@ describe('compose/application-manager', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// A start step should happen for the depended service first
|
// A start step should happen for the dependant service first
|
||||||
expect(startStep).to.have.property('action').that.equals('start');
|
expect(startStep).to.have.property('action').that.equals('start');
|
||||||
expect(startStep)
|
expect(startStep)
|
||||||
.to.have.property('target')
|
.to.have.property('target')
|
||||||
|
@ -4,6 +4,7 @@ import { stub } from 'sinon';
|
|||||||
import Docker from 'dockerode';
|
import Docker from 'dockerode';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { setTimeout } from 'timers/promises';
|
import { setTimeout } from 'timers/promises';
|
||||||
|
import { testfs } from 'mocha-pod';
|
||||||
|
|
||||||
import * as deviceState from '~/src/device-state';
|
import * as deviceState from '~/src/device-state';
|
||||||
import * as config from '~/src/config';
|
import * as config from '~/src/config';
|
||||||
@ -11,10 +12,12 @@ import * as hostConfig from '~/src/host-config';
|
|||||||
import * as deviceApi from '~/src/device-api';
|
import * as deviceApi from '~/src/device-api';
|
||||||
import * as actions from '~/src/device-api/actions';
|
import * as actions from '~/src/device-api/actions';
|
||||||
import * as TargetState from '~/src/device-state/target-state';
|
import * as TargetState from '~/src/device-state/target-state';
|
||||||
|
import * as updateLock from '~/lib/update-lock';
|
||||||
|
import { pathOnRoot } from '~/lib/host-utils';
|
||||||
|
import { exec } from '~/lib/fs-utils';
|
||||||
|
import * as lockfile from '~/lib/lockfile';
|
||||||
import { cleanupDocker } from '~/test-lib/docker-helper';
|
import { cleanupDocker } from '~/test-lib/docker-helper';
|
||||||
|
|
||||||
import { exec } from '~/src/lib/fs-utils';
|
|
||||||
|
|
||||||
export async function dbusSend(
|
export async function dbusSend(
|
||||||
dest: string,
|
dest: string,
|
||||||
path: string,
|
path: string,
|
||||||
@ -79,6 +82,7 @@ describe('manages application lifecycle', () => {
|
|||||||
const BALENA_SUPERVISOR_ADDRESS =
|
const BALENA_SUPERVISOR_ADDRESS =
|
||||||
process.env.BALENA_SUPERVISOR_ADDRESS || 'http://balena-supervisor:48484';
|
process.env.BALENA_SUPERVISOR_ADDRESS || 'http://balena-supervisor:48484';
|
||||||
const APP_ID = 1;
|
const APP_ID = 1;
|
||||||
|
const lockdir = pathOnRoot(updateLock.BASE_LOCK_DIR);
|
||||||
const docker = new Docker();
|
const docker = new Docker();
|
||||||
|
|
||||||
const getSupervisorTarget = async () =>
|
const getSupervisorTarget = async () =>
|
||||||
@ -218,6 +222,11 @@ describe('manages application lifecycle', () => {
|
|||||||
ctns.every(({ State }) => !startedAt.includes(State.StartedAt));
|
ctns.every(({ State }) => !startedAt.includes(State.StartedAt));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockFs = testfs(
|
||||||
|
{ [`${lockdir}/${APP_ID}`]: {} },
|
||||||
|
{ cleanup: [`${lockdir}/${APP_ID}/**/*.lock`] },
|
||||||
|
);
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// Images are ignored in local mode so we need to pull the base image
|
// Images are ignored in local mode so we need to pull the base image
|
||||||
await docker.pull(BASE_IMAGE);
|
await docker.pull(BASE_IMAGE);
|
||||||
@ -251,10 +260,16 @@ describe('manages application lifecycle', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await mockFs.enable();
|
||||||
|
|
||||||
// Create a single-container application in local mode
|
// Create a single-container application in local mode
|
||||||
await setSupervisorTarget(targetState);
|
await setSupervisorTarget(targetState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await mockFs.restore();
|
||||||
|
});
|
||||||
|
|
||||||
// Make sure the app is running and correct before testing more assertions
|
// Make sure the app is running and correct before testing more assertions
|
||||||
it('should setup a single container app (sanity check)', async () => {
|
it('should setup a single container app (sanity check)', async () => {
|
||||||
containers = await waitForSetup(targetState);
|
containers = await waitForSetup(targetState);
|
||||||
@ -292,6 +307,69 @@ describe('manages application lifecycle', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not restart an application when user locks are present', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post(`/v1/restart`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ appId: APP_ID }))
|
||||||
|
.expect(423);
|
||||||
|
|
||||||
|
// Containers should not have been restarted
|
||||||
|
const containersAfterRestart = await waitForSetup(targetState);
|
||||||
|
expect(
|
||||||
|
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||||
|
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||||
|
|
||||||
|
// Remove the lock
|
||||||
|
await lockfile.unlock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restart an application when user locks are present if force is specified', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
const isRestartSuccessful = startTimesChanged(
|
||||||
|
containers.map((ctn) => ctn.State.StartedAt),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post(`/v1/restart`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ appId: APP_ID, force: true }));
|
||||||
|
|
||||||
|
const restartedContainers = await waitForSetup(
|
||||||
|
targetState,
|
||||||
|
isRestartSuccessful,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Technically the wait function above should already verify that the two
|
||||||
|
// containers have been restarted, but verify explcitly with an assertion
|
||||||
|
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||||
|
|
||||||
|
// Containers should have different Ids since they're recreated
|
||||||
|
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||||
|
containers.map((ctn) => ctn.Id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait briefly for state to settle which includes releasing locks
|
||||||
|
await setTimeout(1000);
|
||||||
|
|
||||||
|
// User lock should be overridden
|
||||||
|
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should restart service by removing and recreating corresponding container', async () => {
|
it('should restart service by removing and recreating corresponding container', async () => {
|
||||||
containers = await waitForSetup(targetState);
|
containers = await waitForSetup(targetState);
|
||||||
const isRestartSuccessful = startTimesChanged(
|
const isRestartSuccessful = startTimesChanged(
|
||||||
@ -321,6 +399,73 @@ describe('manages application lifecycle', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Since restart-service follows the same code paths as start|stop-service,
|
||||||
|
// these lock test cases should be sufficient to cover all three service actions.
|
||||||
|
it('should not restart service when user locks are present', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post('/v2/applications/1/restart-service')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ serviceName: serviceNames[0] }))
|
||||||
|
.expect(423);
|
||||||
|
|
||||||
|
// Containers should not have been restarted
|
||||||
|
const containersAfterRestart = await waitForSetup(targetState);
|
||||||
|
expect(
|
||||||
|
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||||
|
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||||
|
|
||||||
|
// Remove the lock
|
||||||
|
await lockfile.unlock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restart service when user locks are present if force is specified', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
const isRestartSuccessful = startTimesChanged(
|
||||||
|
containers
|
||||||
|
.filter((ctn) => ctn.Name.includes(serviceNames[0]))
|
||||||
|
.map((ctn) => ctn.State.StartedAt),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post('/v2/applications/1/restart-service')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ serviceName: serviceNames[0], force: true }));
|
||||||
|
|
||||||
|
const restartedContainers = await waitForSetup(
|
||||||
|
targetState,
|
||||||
|
isRestartSuccessful,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Technically the wait function above should already verify that the two
|
||||||
|
// containers have been restarted, but verify explcitly with an assertion
|
||||||
|
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||||
|
|
||||||
|
// Containers should have different Ids since they're recreated
|
||||||
|
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||||
|
containers.map((ctn) => ctn.Id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait briefly for state to settle which includes releasing locks
|
||||||
|
await setTimeout(1000);
|
||||||
|
|
||||||
|
// User lock should be overridden
|
||||||
|
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should stop a running service', async () => {
|
it('should stop a running service', async () => {
|
||||||
containers = await waitForSetup(targetState);
|
containers = await waitForSetup(targetState);
|
||||||
|
|
||||||
@ -520,10 +665,15 @@ describe('manages application lifecycle', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await mockFs.enable();
|
||||||
// Create a multi-container application in local mode
|
// Create a multi-container application in local mode
|
||||||
await setSupervisorTarget(targetState);
|
await setSupervisorTarget(targetState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await mockFs.restore();
|
||||||
|
});
|
||||||
|
|
||||||
// Make sure the app is running and correct before testing more assertions
|
// Make sure the app is running and correct before testing more assertions
|
||||||
it('should setup a multi-container app (sanity check)', async () => {
|
it('should setup a multi-container app (sanity check)', async () => {
|
||||||
containers = await waitForSetup(targetState);
|
containers = await waitForSetup(targetState);
|
||||||
@ -560,6 +710,69 @@ describe('manages application lifecycle', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not restart an application when user locks are present', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post(`/v1/restart`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ appId: APP_ID }))
|
||||||
|
.expect(423);
|
||||||
|
|
||||||
|
// Containers should not have been restarted
|
||||||
|
const containersAfterRestart = await waitForSetup(targetState);
|
||||||
|
expect(
|
||||||
|
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||||
|
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||||
|
|
||||||
|
// Remove the lock
|
||||||
|
await lockfile.unlock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restart an application when user locks are present if force is specified', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
const isRestartSuccessful = startTimesChanged(
|
||||||
|
containers.map((ctn) => ctn.State.StartedAt),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post(`/v1/restart`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ appId: APP_ID, force: true }));
|
||||||
|
|
||||||
|
const restartedContainers = await waitForSetup(
|
||||||
|
targetState,
|
||||||
|
isRestartSuccessful,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Technically the wait function above should already verify that the two
|
||||||
|
// containers have been restarted, but verify explcitly with an assertion
|
||||||
|
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||||
|
|
||||||
|
// Containers should have different Ids since they're recreated
|
||||||
|
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||||
|
containers.map((ctn) => ctn.Id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait briefly for state to settle which includes releasing locks
|
||||||
|
await setTimeout(500);
|
||||||
|
|
||||||
|
// User lock should be overridden
|
||||||
|
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should restart service by removing and recreating corresponding container', async () => {
|
it('should restart service by removing and recreating corresponding container', async () => {
|
||||||
containers = await waitForSetup(targetState);
|
containers = await waitForSetup(targetState);
|
||||||
const serviceName = serviceNames[0];
|
const serviceName = serviceNames[0];
|
||||||
@ -601,6 +814,73 @@ describe('manages application lifecycle', () => {
|
|||||||
expect(sharedIds.length).to.equal(1);
|
expect(sharedIds.length).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Since restart-service follows the same code paths as start|stop-service,
|
||||||
|
// these lock test cases should be sufficient to cover all three service actions.
|
||||||
|
it('should not restart service when user locks are present', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post('/v2/applications/1/restart-service')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ serviceName: serviceNames[0] }))
|
||||||
|
.expect(423);
|
||||||
|
|
||||||
|
// Containers should not have been restarted
|
||||||
|
const containersAfterRestart = await waitForSetup(targetState);
|
||||||
|
expect(
|
||||||
|
containersAfterRestart.map((ctn) => ctn.State.StartedAt),
|
||||||
|
).to.deep.include.members(containers.map((ctn) => ctn.State.StartedAt));
|
||||||
|
|
||||||
|
// Remove the lock
|
||||||
|
await lockfile.unlock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restart service when user locks are present if force is specified', async () => {
|
||||||
|
containers = await waitForSetup(targetState);
|
||||||
|
const isRestartSuccessful = startTimesChanged(
|
||||||
|
containers
|
||||||
|
.filter((ctn) => ctn.Name.includes(serviceNames[0]))
|
||||||
|
.map((ctn) => ctn.State.StartedAt),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a lock
|
||||||
|
await lockfile.lock(
|
||||||
|
`${lockdir}/${APP_ID}/${serviceNames[0]}/updates.lock`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(BALENA_SUPERVISOR_ADDRESS)
|
||||||
|
.post('/v2/applications/1/restart-service')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(JSON.stringify({ serviceName: serviceNames[0], force: true }));
|
||||||
|
|
||||||
|
const restartedContainers = await waitForSetup(
|
||||||
|
targetState,
|
||||||
|
isRestartSuccessful,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Technically the wait function above should already verify that the two
|
||||||
|
// containers have been restarted, but verify explcitly with an assertion
|
||||||
|
expect(isRestartSuccessful(restartedContainers)).to.be.true;
|
||||||
|
|
||||||
|
// Containers should have different Ids since they're recreated
|
||||||
|
expect(restartedContainers.map(({ Id }) => Id)).to.not.have.members(
|
||||||
|
containers.map((ctn) => ctn.Id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait briefly for state to settle which includes releasing locks
|
||||||
|
await setTimeout(500);
|
||||||
|
|
||||||
|
// User lock should be overridden
|
||||||
|
expect(await updateLock.getLocksTaken()).to.deep.equal([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should stop a running service', async () => {
|
it('should stop a running service', async () => {
|
||||||
containers = await waitForSetup(targetState);
|
containers = await waitForSetup(targetState);
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
|
|
||||||
const defaultContext = {
|
const defaultContext = {
|
||||||
keepVolumes: false,
|
keepVolumes: false,
|
||||||
|
force: false,
|
||||||
availableImages: [] as Image[],
|
availableImages: [] as Image[],
|
||||||
containerIds: {},
|
containerIds: {},
|
||||||
downloading: [] as string[],
|
downloading: [] as string[],
|
||||||
@ -151,6 +152,7 @@ describe('compose/app', () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
||||||
|
networks: [DEFAULT_NETWORK],
|
||||||
});
|
});
|
||||||
const target = createApp({
|
const target = createApp({
|
||||||
services: [
|
services: [
|
||||||
@ -164,6 +166,7 @@ describe('compose/app', () => {
|
|||||||
labels: { test: 'test' },
|
labels: { test: 'test' },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
networks: [DEFAULT_NETWORK],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -453,9 +456,10 @@ describe('compose/app', () => {
|
|||||||
|
|
||||||
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
||||||
|
|
||||||
const [removeNetworkStep] = expectSteps('kill', steps);
|
const [killStep] = expectSteps('kill', steps);
|
||||||
|
console.log(killStep);
|
||||||
|
|
||||||
expect(removeNetworkStep).to.have.property('current').that.deep.includes({
|
expect(killStep).to.have.property('current').that.deep.includes({
|
||||||
serviceName: 'test',
|
serviceName: 'test',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -576,7 +580,7 @@ describe('compose/app', () => {
|
|||||||
services: [
|
services: [
|
||||||
await createService({
|
await createService({
|
||||||
serviceName: 'test',
|
serviceName: 'test',
|
||||||
composition: { networks: ['test-network'] },
|
composition: { networks: { 'test-network': {} } },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
networks: [Network.fromComposeObject('test-network', 1, 'appuuid', {})],
|
networks: [Network.fromComposeObject('test-network', 1, 'appuuid', {})],
|
||||||
@ -977,7 +981,6 @@ describe('compose/app', () => {
|
|||||||
// Take lock before updating metadata
|
// Take lock before updating metadata
|
||||||
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
||||||
const [takeLockStep] = expectSteps('takeLock', steps);
|
const [takeLockStep] = expectSteps('takeLock', steps);
|
||||||
expect(takeLockStep).to.have.property('appId').that.equals(1);
|
|
||||||
expect(takeLockStep)
|
expect(takeLockStep)
|
||||||
.to.have.property('services')
|
.to.have.property('services')
|
||||||
.that.deep.equals(['main']);
|
.that.deep.equals(['main']);
|
||||||
@ -1080,7 +1083,7 @@ describe('compose/app', () => {
|
|||||||
.to.have.property('current')
|
.to.have.property('current')
|
||||||
.that.deep.includes({ serviceName: 'main' });
|
.that.deep.includes({ serviceName: 'main' });
|
||||||
|
|
||||||
// assume the intermediate step has already removed the app
|
// Assume the intermediate step has already removed the app
|
||||||
const intermediate = createApp({
|
const intermediate = createApp({
|
||||||
services: [],
|
services: [],
|
||||||
// Default network was already created
|
// Default network was already created
|
||||||
@ -1092,7 +1095,6 @@ describe('compose/app', () => {
|
|||||||
contextWithImages,
|
contextWithImages,
|
||||||
target,
|
target,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [startStep] = expectSteps('start', stepsToTarget);
|
const [startStep] = expectSteps('start', stepsToTarget);
|
||||||
expect(startStep)
|
expect(startStep)
|
||||||
.to.have.property('target')
|
.to.have.property('target')
|
||||||
@ -1173,7 +1175,6 @@ describe('compose/app', () => {
|
|||||||
{ ...contextWithImages, ...{ containerIds: { dep: 'dep-id' } } },
|
{ ...contextWithImages, ...{ containerIds: { dep: 'dep-id' } } },
|
||||||
target,
|
target,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [startMainStep] = expectSteps('start', stepsToTarget);
|
const [startMainStep] = expectSteps('start', stepsToTarget);
|
||||||
expect(startMainStep)
|
expect(startMainStep)
|
||||||
.to.have.property('target')
|
.to.have.property('target')
|
||||||
@ -1470,6 +1471,7 @@ describe('compose/app', () => {
|
|||||||
commit: 'old-release',
|
commit: 'old-release',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
networks: [DEFAULT_NETWORK],
|
||||||
});
|
});
|
||||||
const target = createApp({
|
const target = createApp({
|
||||||
services: [
|
services: [
|
||||||
@ -1479,6 +1481,7 @@ describe('compose/app', () => {
|
|||||||
commit: 'new-release',
|
commit: 'new-release',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
networks: [DEFAULT_NETWORK],
|
||||||
isTarget: true,
|
isTarget: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user