Merge pull request #1753 from balena-os/no-db-ids

Remove comparisons based on image, release, and service ids
This commit is contained in:
bulldozer-balena[bot] 2021-07-30 22:25:38 +00:00 committed by GitHub
commit 27013b1d72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 566 additions and 681 deletions

View File

@ -31,7 +31,6 @@ export interface AppConstructOpts {
appId: number;
appName?: string;
commit?: string;
releaseId?: number;
source?: string;
services: Service[];
@ -43,7 +42,7 @@ export interface UpdateState {
localMode: boolean;
availableImages: Image[];
containerIds: Dictionary<string>;
downloading: number[];
downloading: string[];
}
interface ChangingPair<T> {
@ -56,7 +55,6 @@ export class App {
// When setting up an application from current state, these values are not available
public appName?: string;
public commit?: string;
public releaseId?: number;
public source?: string;
// Services are stored as an array, as at any one time we could have more than one
@ -69,7 +67,6 @@ export class App {
this.appId = opts.appId;
this.appName = opts.appName;
this.commit = opts.commit;
this.releaseId = opts.releaseId;
this.source = opts.source;
this.services = opts.services;
this.volumes = opts.volumes;
@ -266,34 +263,30 @@ export class App {
removePairs: Array<ChangingPair<Service>>;
updatePairs: Array<ChangingPair<Service>>;
} {
const currentByServiceId = _.keyBy(current, 'serviceId');
const targetByServiceId = _.keyBy(target, 'serviceId');
const currentByServiceName = _.keyBy(current, 'serviceName');
const targetByServiceName = _.keyBy(target, 'serviceName');
const currentServiceIds = Object.keys(currentByServiceId).map((i) =>
parseInt(i, 10),
);
const targetServiceIds = Object.keys(targetByServiceId).map((i) =>
parseInt(i, 10),
);
const currentServiceNames = Object.keys(currentByServiceName);
const targetServiceNames = Object.keys(targetByServiceName);
const toBeRemoved = _(currentServiceIds)
.difference(targetServiceIds)
.map((id) => ({ current: currentByServiceId[id] }))
const toBeRemoved = _(currentServiceNames)
.difference(targetServiceNames)
.map((id) => ({ current: currentByServiceName[id] }))
.value();
const toBeInstalled = _(targetServiceIds)
.difference(currentServiceIds)
.map((id) => ({ target: targetByServiceId[id] }))
const toBeInstalled = _(targetServiceNames)
.difference(currentServiceNames)
.map((id) => ({ target: targetByServiceName[id] }))
.value();
const maybeUpdate = _.intersection(targetServiceIds, currentServiceIds);
const maybeUpdate = _.intersection(targetServiceNames, currentServiceNames);
// Build up a list of services for a given service ID, always using the latest created
// Build up a list of services for a given service name, always using the latest created
// service. Any older services will have kill steps emitted
for (const serviceId of maybeUpdate) {
const currentServiceContainers = _.filter(current, { serviceId });
for (const serviceName of maybeUpdate) {
const currentServiceContainers = _.filter(current, { serviceName });
if (currentServiceContainers.length > 1) {
currentByServiceId[serviceId] = _.maxBy(
currentByServiceName[serviceName] = _.maxBy(
currentServiceContainers,
'createdAt',
)!;
@ -302,13 +295,13 @@ export class App {
// be removed
const otherContainers = _.without(
currentServiceContainers,
currentByServiceId[serviceId],
currentByServiceName[serviceName],
);
for (const service of otherContainers) {
toBeRemoved.push({ current: service });
}
} else {
currentByServiceId[serviceId] = currentServiceContainers[0];
currentByServiceName[serviceName] = currentServiceContainers[0];
}
}
@ -374,9 +367,9 @@ export class App {
* Filter all the services which should be updated due to run state change, or config mismatch.
*/
const toBeUpdated = maybeUpdate
.map((serviceId) => ({
current: currentByServiceId[serviceId],
target: targetByServiceId[serviceId],
.map((serviceName) => ({
current: currentByServiceName[serviceName],
target: targetByServiceName[serviceName],
}))
.filter(
({ current: c, target: t }) =>
@ -456,7 +449,7 @@ export class App {
context: {
localMode: boolean;
availableImages: Image[];
downloading: number[];
downloading: string[];
targetApp: App;
containerIds: Dictionary<string>;
networkPairs: Array<ChangingPair<Network>>;
@ -486,7 +479,7 @@ export class App {
);
}
if (needsDownload && context.downloading.includes(target?.imageId!)) {
if (needsDownload && context.downloading.includes(target?.imageName!)) {
// The image needs to be downloaded, and it's currently downloading. We simply keep
// the application loop alive
return generateStep('noop', {});
@ -563,7 +556,7 @@ export class App {
service.status !== 'Stopping' &&
!_.some(
changingServices,
({ current }) => current?.serviceId !== service.serviceId,
({ current }) => current?.serviceName !== service.serviceName,
)
) {
return [generateStep('kill', { current: service })];
@ -595,11 +588,8 @@ export class App {
}
private generateContainerStep(current: Service, target: Service) {
// if the services release/image don't match, then rename the container...
if (
current.releaseId !== target.releaseId ||
current.imageId !== target.imageId
) {
// if the services release doesn't match, then rename the container...
if (current.commit !== target.commit) {
return generateStep('updateMetadata', { current, target });
} else if (target.config.running !== current.config.running) {
if (target.config.running) {
@ -728,7 +718,7 @@ export class App {
!_.some(
availableImages,
(image) =>
image.dockerImageId === dependencyService?.imageId ||
image.dockerImageId === dependencyService?.config.image ||
imageManager.isSameImage(image, {
name: dependencyService?.imageName!,
}),
@ -825,7 +815,6 @@ export class App {
{
appId: app.appId,
commit: app.commit,
releaseId: app.releaseId,
appName: app.name,
source: app.source,
services,

View File

@ -125,7 +125,6 @@ let targetVolatilePerImageId: {
export const initialized = (async () => {
await config.initialized;
await imageManager.initialized;
await imageManager.cleanImageData();
const cleanup = async () => {
const containers = await docker.listContainers({ all: true });
@ -180,7 +179,7 @@ export async function getRequiredSteps(
): Promise<CompositionStep[]> {
// get some required data
const [downloading, availableImages, currentApps] = await Promise.all([
imageManager.getDownloadingImageIds(),
imageManager.getDownloadingImageNames(),
imageManager.getAvailable(),
getCurrentApps(),
]);
@ -200,7 +199,7 @@ export async function inferNextSteps(
targetApps: InstancedAppState,
{
ignoreImages = false,
downloading = [] as number[],
downloading = [] as string[],
availableImages = [] as Image[],
containerIdsByAppId = {} as { [appId: number]: Dictionary<string> },
} = {},
@ -675,7 +674,7 @@ function saveAndRemoveImages(
(svc) =>
_.find(availableImages, {
dockerImageId: svc.config.image,
imageId: svc.imageId,
name: svc.imageName,
}) ?? _.find(availableImages, { dockerImageId: svc.config.image }),
),
) as imageManager.Image[];

View File

@ -15,7 +15,6 @@ import {
StatusError,
} from '../lib/errors';
import * as LogTypes from '../lib/log-types';
import * as validation from '../lib/validation';
import * as logger from '../logger';
import { ImageDownloadBackoffError } from './errors';
@ -68,20 +67,90 @@ const imageFetchLastFailureTime: Dictionary<ReturnType<
typeof process.hrtime
>> = {};
const imageCleanupFailures: Dictionary<number> = {};
// A store of volatile state for images (e.g. download progress), indexed by imageId
const volatileState: { [imageId: number]: Image } = {};
let appUpdatePollInterval: number;
type ImageState = Pick<Image, 'status' | 'downloadProgress'>;
type ImageTask = {
// Indicates whether the task has been finished
done?: boolean;
export const initialized = (async () => {
await config.initialized;
appUpdatePollInterval = await config.get('appUpdatePollInterval');
config.on('change', (vals) => {
if (vals.appUpdatePollInterval != null) {
appUpdatePollInterval = vals.appUpdatePollInterval;
}
});
})();
// Current image state of the task
context: Image;
// Update the task with new context. This is a pure function
// meaning it doesn't modify the original task
update: (change?: ImageState) => ImageTaskUpdate;
// Finish the task. This is a pure function
// meaning it doesn't modify the original task
finish: () => ImageTaskUpdate;
};
type ImageTaskUpdate = [ImageTask, boolean];
// Create new running task with the given initial context
function createTask(initialContext: Image) {
// Task has only two state, is either running or finished
const running = (context: Image): ImageTask => {
return {
context,
update: ({ status, downloadProgress }: ImageState) =>
// Keep current state
[
running({
...context,
...(status && { status }),
...(downloadProgress && { downloadProgress }),
}),
// Only mark the task as changed if there is new data
[status, downloadProgress].some((v) => !!v),
],
finish: () => [finished(context), true],
};
};
// Once the task is finished, it cannot go back to a running state
const finished = (context: Image): ImageTask => {
return {
done: true,
context,
update: () => [finished(context), false],
finish: () => [finished(context), false],
};
};
return running(initialContext);
}
const runningTasks: { [imageName: string]: ImageTask } = {};
function reportEvent(event: 'start' | 'update' | 'finish', state: Image) {
const { name: imageName } = state;
// Emit by default if a start event is reported
let emitChange = event === 'start';
// Get the current task and update it in memory
const currentTask =
event === 'start' ? createTask(state) : runningTasks[imageName];
runningTasks[imageName] = currentTask;
// TODO: should we assert that the current task exists at this point?
// On update, update the corresponding task with the new state if it exists
if (event === 'update' && currentTask) {
const [updatedTask, changed] = currentTask.update(state);
runningTasks[imageName] = updatedTask;
emitChange = changed;
}
// On update, update the corresponding task with the new state if it exists
if (event === 'finish' && currentTask) {
[, emitChange] = currentTask.finish();
delete runningTasks[imageName];
}
if (emitChange) {
events.emit('change');
}
}
type ServiceInfo = Pick<
Service,
@ -106,6 +175,8 @@ export async function triggerFetch(
onFinish = _.noop,
serviceName: string,
): Promise<void> {
const appUpdatePollInterval = await config.get('appUpdatePollInterval');
if (imageFetchFailures[image.name] != null) {
// If we are retrying a pull within the backoff time of the last failure,
// we need to throw an error, which will be caught in the device-state
@ -125,12 +196,7 @@ export async function triggerFetch(
}
const onProgress = (progress: FetchProgressEvent) => {
// Only report the percentage if we haven't finished fetching
if (volatileState[image.imageId] != null) {
reportChange(image.imageId, {
downloadProgress: progress.percentage,
});
}
reportEvent('update', { ...image, downloadProgress: progress.percentage });
};
let success: boolean;
@ -157,10 +223,13 @@ export async function triggerFetch(
}
throw e;
}
reportChange(
image.imageId,
_.merge(_.clone(image), { status: 'Downloading', downloadProgress: 0 }),
);
// Report a fetch start
reportEvent('start', {
...image,
status: 'Downloading',
downloadProgress: 0,
});
try {
let id;
@ -197,7 +266,7 @@ export async function triggerFetch(
}
}
reportChange(image.imageId);
reportEvent('finish', image);
onFinish(success);
}
@ -280,9 +349,15 @@ export async function getAvailable(): Promise<Image[]> {
}
export function getDownloadingImageIds(): number[] {
return _.keys(_.pickBy(volatileState, { status: 'Downloading' })).map((i) =>
validation.checkInt(i),
) as number[];
return Object.values(runningTasks)
.filter((t) => t.context.status === 'Downloading')
.map((t) => t.context.imageId);
}
export function getDownloadingImageNames(): string[] {
return Object.values(runningTasks)
.filter((t) => t.context.status === 'Downloading')
.map((t) => t.context.name);
}
export async function cleanImageData(): Promise<void> {
@ -331,18 +406,22 @@ export async function cleanImageData(): Promise<void> {
}
export const getStatus = async () => {
const images = await getAvailable();
for (const image of images) {
image.status = 'Downloaded';
image.downloadProgress = null;
}
const status = _.clone(volatileState);
for (const image of images) {
if (status[image.imageId] == null) {
status[image.imageId] = image;
}
}
return _.values(status);
const images = (await getAvailable()).map((img) => ({
...img,
status: 'Downloaded' as Image['status'],
downloadImageSuccess: null,
}));
const imagesFromRunningTasks = Object.values(runningTasks).map(
(task) => task.context,
);
const runningImageIds = imagesFromRunningTasks.map((img) => img.imageId);
// TODO: this is possibly wrong, the value from getAvailable should be more reliable
// than the value from running tasks
return imagesFromRunningTasks.concat(
images.filter((img) => !runningImageIds.includes(img.imageId)),
);
};
export async function update(image: Image): Promise<void> {
@ -593,10 +672,7 @@ async function removeImageIfNotNeeded(image: Image): Promise<void> {
[] as string[],
);
reportChange(
image.imageId,
_.merge(_.clone(image), { status: 'Deleting' }),
);
reportEvent('start', { ...image, status: 'Deleting' });
logger.logSystemEvent(LogTypes.deleteImage, { image });
// The engine doesn't handle concurrency too well. If two requests to
@ -641,7 +717,7 @@ async function removeImageIfNotNeeded(image: Image): Promise<void> {
throw e;
}
} finally {
reportChange(image.imageId);
reportEvent('finish', image);
}
await db.models('image').del().where({ id: img.id });
@ -706,20 +782,3 @@ function fetchImage(
logger.logSystemEvent(LogTypes.downloadImage, { image });
return dockerUtils.fetchImageWithProgress(image.name, opts, onProgress);
}
// TODO: find out if imageId can actually be null
function reportChange(imageId: Nullable<number>, status?: Partial<Image>) {
if (imageId == null) {
return;
}
if (status != null) {
if (volatileState[imageId] == null) {
volatileState[imageId] = { imageId } as Image;
}
_.merge(volatileState[imageId], status);
return events.emit('change');
} else if (volatileState[imageId] != null) {
delete volatileState[imageId];
return events.emit('change');
}
}

View File

@ -20,7 +20,7 @@ import {
} from '../lib/errors';
import * as LogTypes from '../lib/log-types';
import { checkInt, isValidDeviceName } from '../lib/validation';
import { Service } from './service';
import { Service, ServiceStatus } from './service';
import { serviceNetworksToDockerNetworks } from './utils';
import log from '../lib/supervisor-console';
@ -88,7 +88,7 @@ export async function get(service: Service) {
// Get the container ids for special network handling
const containerIds = await getContainerIdMap(service.appId!);
const services = (
await getAll(`service-id=${service.serviceId}`)
await getAll(`service-name=${service.serviceName}`)
).filter((currentService) =>
currentService.isEqualConfig(service, containerIds),
);
@ -151,7 +151,7 @@ export async function updateMetadata(service: Service, target: Service) {
}
await docker.getContainer(svc.containerId).rename({
name: `${service.serviceName}_${target.imageId}_${target.releaseId}`,
name: `${service.serviceName}_${target.imageId}_${target.releaseId}_${target.commit}`,
});
}
@ -294,7 +294,7 @@ export async function start(service: Service) {
containerId = container.id;
logger.logSystemEvent(LogTypes.startService, { service });
reportNewStatus(containerId, service, 'Starting');
reportNewStatus(containerId, service, 'Starting' as ServiceStatus);
let shouldRemove = false;
let err: Error | undefined;
@ -498,7 +498,7 @@ function reportChange(containerId?: string, status?: Partial<Service>) {
function reportNewStatus(
containerId: string,
service: Partial<Service>,
status: string,
status: ServiceStatus,
) {
reportChange(
containerId,
@ -611,7 +611,7 @@ async function prepareForHandover(service: Service) {
const container = docker.getContainer(svc.containerId);
await container.update({ RestartPolicy: {} });
return await container.rename({
name: `old_${service.serviceName}_${service.imageId}_${service.imageId}_${service.releaseId}`,
name: `old_${service.serviceName}_${service.imageId}_${service.releaseId}_${service.commit}`,
});
}

View File

@ -44,7 +44,8 @@ export class Service {
public appId: number;
public imageId: number;
public config: ServiceConfig;
public serviceName: string | null;
public serviceName: string;
public commit: string;
public releaseId: number;
public serviceId: number;
public imageName: string | null;
@ -135,12 +136,11 @@ export class Service {
delete appConfig.dependsOn;
service.createdAt = appConfig.createdAt;
delete appConfig.createdAt;
service.commit = appConfig.commit;
delete appConfig.commit;
delete appConfig.contract;
// We don't need this value
delete appConfig.commit;
// Get rid of any extra values and report them to the user
const config = sanitiseComposeConfig(appConfig);
@ -600,15 +600,16 @@ export class Service {
'Attempt to build Service class from container with malformed labels',
);
}
const nameMatch = container.Name.match(/.*_(\d+)_(\d+)$/);
const nameMatch = container.Name.match(/.*_(\d+)_(\d+)(?:_(.*?))?$/);
if (nameMatch == null) {
throw new InternalInconsistencyError(
'Attempt to build Service class from container with malformed name',
`Expected supervised container to have name '<serviceName>_<imageId>_<releaseId>_<commit>', got: ${container.Name}`,
);
}
svc.imageId = parseInt(nameMatch[1], 10);
svc.releaseId = parseInt(nameMatch[2], 10);
svc.commit = nameMatch[3];
svc.containerId = container.Id;
svc.dockerImageId = container.Config.Image;
@ -656,7 +657,7 @@ export class Service {
this.config.networkMode = `container:${containerId}`;
}
return {
name: `${this.serviceName}_${this.imageId}_${this.releaseId}`,
name: `${this.serviceName}_${this.imageId}_${this.releaseId}_${this.commit}`,
Tty: this.config.tty,
Cmd: this.config.command,
Volumes: volumes,
@ -862,8 +863,7 @@ export class Service {
): boolean {
return (
this.isEqualConfig(service, currentContainerIds) &&
this.releaseId === service.releaseId &&
this.imageId === service.imageId
this.commit === service.commit
);
}

View File

@ -373,7 +373,7 @@ export async function addFeaturesFromLabels(
// create a app/service specific API secret
const apiSecret = await apiKeys.generateScopedKey(
service.appId,
service.serviceId,
service.serviceName,
);
const host = (() => {

View File

@ -121,13 +121,6 @@ export async function doPurge(appId, force) {
});
}
export function serviceAction(action, serviceId, current, target, options) {
if (options == null) {
options = {};
}
return { action, serviceId, current, target, options };
}
/**
* This doesn't truly return an InstancedDeviceState, but it's close enough to mostly work where it's used
*

View File

@ -15,7 +15,7 @@ export class KeyNotFoundError extends Error {}
interface DbApiSecret {
id: number;
appId: number;
serviceId: number;
serviceName: string;
scopes: string;
key: string;
}
@ -199,17 +199,17 @@ export async function getScopesForKey(key: string): Promise<Scope[] | null> {
export async function generateScopedKey(
appId: number,
serviceId: number,
serviceName: string,
options?: Partial<GenerateKeyOptions>,
): Promise<string> {
await initialized;
return await generateKey(appId, serviceId, options);
return await generateKey(appId, serviceName, options);
}
export async function generateCloudKey(
force: boolean = false,
): Promise<string> {
cloudApiKey = await generateKey(0, 0, {
cloudApiKey = await generateKey(0, null, {
force,
scopes: [{ type: 'global' }],
});
@ -223,15 +223,15 @@ export async function refreshKey(key: string): Promise<string> {
throw new KeyNotFoundError();
}
const { appId, serviceId, scopes } = apiKey;
const { appId, serviceName, scopes } = apiKey;
// if this is a cloud key that is being refreshed
if (appId === 0 && serviceId === 0) {
if (appId === 0 && serviceName === null) {
return await generateCloudKey(true);
}
// generate a new key, expiring the old one...
const newKey = await generateScopedKey(appId, serviceId, {
const newKey = await generateScopedKey(appId, serviceName, {
force: true,
scopes: deserialiseScopes(scopes),
});
@ -244,15 +244,15 @@ export async function refreshKey(key: string): Promise<string> {
* A cached lookup of the database key
*/
const getApiKeyForService = memoizee(
async (appId: number, serviceId: number): Promise<DbApiSecret[]> => {
async (appId: number, serviceName: string | null): Promise<DbApiSecret[]> => {
await db.initialized;
return await db.models('apiSecret').where({ appId, serviceId }).select();
return await db.models('apiSecret').where({ appId, serviceName }).select();
},
{
promise: true,
maxAge: 60000, // 1 minute
normalizer: ([appId, serviceId]) => `${appId}-${serviceId}`,
normalizer: ([appId, serviceName]) => `${appId}-${serviceName}`,
},
);
@ -276,12 +276,12 @@ const getApiKeyByKey = memoizee(
* All key generate logic should come though this method. It handles cache clearing.
*
* @param appId
* @param serviceId
* @param serviceName
* @param options
*/
async function generateKey(
appId: number,
serviceId: number,
serviceName: string | null,
options?: Partial<GenerateKeyOptions>,
): Promise<string> {
// set default options
@ -292,13 +292,13 @@ async function generateKey(
};
// grab the existing API key info
const secrets = await getApiKeyForService(appId, serviceId);
const secrets = await getApiKeyForService(appId, serviceName);
// if we need a new key
if (secrets.length === 0 || force) {
// are forcing a new key?
if (force) {
await db.models('apiSecret').where({ appId, serviceId }).del();
await db.models('apiSecret').where({ appId, serviceName }).del();
}
// remove the cached lookup for the key
@ -308,10 +308,10 @@ async function generateKey(
}
// remove the cached value for this lookup
getApiKeyForService.clear(appId, serviceId);
getApiKeyForService.clear(appId, serviceName);
// return a new API key
return await createNewKey(appId, serviceId, scopes);
return await createNewKey(appId, serviceName, scopes);
}
// grab the current secret and scopes
@ -333,21 +333,25 @@ async function generateKey(
}
// forcibly get a new key...
return await generateKey(appId, serviceId, { ...options, force: true });
return await generateKey(appId, serviceName, { ...options, force: true });
}
/**
* Generates a new key value and inserts it into the DB.
*
* @param appId
* @param serviceId
* @param serviceName
* @param scopes
*/
async function createNewKey(appId: number, serviceId: number, scopes: Scope[]) {
async function createNewKey(
appId: number,
serviceName: string | null,
scopes: Scope[],
) {
const key = generateUniqueKey();
await db.models('apiSecret').insert({
appId,
serviceId,
serviceName,
key,
scopes: serialiseScopes(scopes),
});

41
src/migrations/M00007.js Normal file
View File

@ -0,0 +1,41 @@
export async function up(knex) {
// Add serviceName to apiSecret schema
await knex.schema.table('apiSecret', (table) => {
table.string('serviceName');
table.unique(['appId', 'serviceName']);
});
const targetServices = (await knex('app').select(['appId', 'services']))
.map(({ appId, services }) => ({
appId,
// Extract service name and id per app
services: JSON.parse(services).map(({ serviceId, serviceName }) => ({
serviceId,
serviceName,
})),
}))
.reduce(
// Convert to array of {appId, serviceId, serviceName}
(apps, { appId, services }) =>
apps.concat(services.map((svc) => ({ appId, ...svc }))),
[],
);
// Update all API secret entries so services can still access the API after
// the change
await Promise.all(
targetServices.map(({ appId, serviceId, serviceName }) =>
knex('apiSecret').update({ serviceName }).where({ appId, serviceId }),
),
);
// Update the table schema deleting the serviceId column
await knex.schema.table('apiSecret', (table) => {
table.dropUnique(['appId', 'serviceId']);
table.dropColumn('serviceId');
});
}
export function down() {
return Promise.reject(new Error('Not Implemented'));
}

View File

@ -64,7 +64,7 @@ describe('SupervisorAPI', () => {
describe('API Key Scope', () => {
it('should generate a key which is scoped for a single application', async () => {
// single app scoped key...
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
await request
.get('/v2/applications/1/state')
@ -74,7 +74,7 @@ describe('SupervisorAPI', () => {
});
it('should generate a key which is scoped for multiple applications', async () => {
// multi-app scoped key...
const multiAppScopedKey = await apiKeys.generateScopedKey(1, 2, {
const multiAppScopedKey = await apiKeys.generateScopedKey(1, 'other', {
scopes: [1, 2].map((appId) => {
return { type: 'app', appId };
}),
@ -135,7 +135,7 @@ describe('SupervisorAPI', () => {
});
it('should regenerate a key and invalidate the old one', async () => {
// single app scoped key...
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
await request
.get('/v2/applications/1/state')

View File

@ -86,7 +86,6 @@ describe('DB Format', () => {
expect(app).to.be.an.instanceOf(App);
expect(app).to.have.property('appId').that.equals(1);
expect(app).to.have.property('commit').that.equals('abcdef');
expect(app).to.have.property('releaseId').that.equals(123);
expect(app).to.have.property('appName').that.equals('test-app');
expect(app)
.to.have.property('source')

View File

@ -1232,7 +1232,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
// resolve to true for any isScoped check
const scopedKey = await apiKeys.generateScopedKey(
2,
containers[0].serviceId,
containers[0].serviceName,
);
await request

View File

@ -142,7 +142,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
describe('Scoped API Keys', () => {
it('returns 409 because app is out of scope of the key', async () => {
const apiKey = await apiKeys.generateScopedKey(3, 1);
const apiKey = await apiKeys.generateScopedKey(3, 'main');
await request
.get('/v2/applications/2/state')
.set('Accept', 'application/json')
@ -164,7 +164,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should return scoped application', async () => {
// Create scoped key for application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Setup device conditions
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
@ -188,7 +188,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should return no application info due to lack of scope', async () => {
// Create scoped key for wrong application
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
// Setup device conditions
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
@ -211,7 +211,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should return success when device has no applications', async () => {
// Create scoped key for any application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 1658654);
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Setup device conditions
serviceManagerMock.resolves([]);
imagesMock.resolves([]);
@ -234,7 +234,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
it('should only return 1 application when N > 1 applications on device', async () => {
// Create scoped key for application
const appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Setup device conditions
serviceManagerMock.resolves([
mockedAPI.mockService({ appId: 1658654 }),
@ -330,7 +330,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
before(async () => {
// Create scoped key for application
appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Mock target state cache
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
@ -439,7 +439,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
before(async () => {
// Create scoped key for application
appScopedKey = await apiKeys.generateScopedKey(1658654, 640681);
appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
// Mock target state cache
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');

View File

@ -15,9 +15,9 @@ import log from '../../../src/lib/supervisor-console';
const defaultContext = {
localMode: false,
availableImages: [],
availableImages: [] as Image[],
containerIds: {},
downloading: [],
downloading: [] as string[],
};
function createApp({
@ -42,26 +42,22 @@ function createApp({
}
async function createService(
conf = {} as Partial<ServiceComposeConfig>,
{
appId = 1,
serviceName = 'test',
releaseId = 2,
serviceId = 3,
imageId = 4,
state = {} as Partial<Service>,
} = {},
commit = 'test-commit',
...conf
} = {} as Partial<ServiceComposeConfig>,
{ state = {} as Partial<Service>, options = {} as any } = {},
) {
const svc = await Service.fromComposeObject(
{
appId,
serviceName,
releaseId,
serviceId,
imageId,
commit,
...conf,
},
{} as any,
options,
);
// Add additonal configuration
@ -71,6 +67,18 @@ async function createService(
return svc;
}
function createImage(
{
appId = 1,
dependent = 0,
name = 'test-image',
serviceName = 'test',
...extra
} = {} as Partial<Image>,
) {
return { appId, dependent, name, serviceName, ...extra } as Image;
}
const expectSteps = (
action: CompositionStepAction,
steps: CompositionStep[],
@ -298,15 +306,10 @@ describe('compose/app', () => {
...defaultContext,
...{
availableImages: [
{
createImage({
appId: service.appId,
dependent: 0,
imageId: service.imageId,
releaseId: service.releaseId,
serviceId: service.serviceId,
name: 'test-image',
serviceName: service.serviceName,
} as Image,
}),
],
},
};
@ -488,7 +491,7 @@ describe('compose/app', () => {
networks: [Network.fromComposeObject('test-network', 1, {})],
});
const target = createApp({
services: [await createService({})],
services: [await createService()],
networks: [],
isTarget: true,
});
@ -557,24 +560,13 @@ describe('compose/app', () => {
it('should create a kill step for service which is no longer referenced', async () => {
const current = createApp({
services: [
await createService(
{},
{ appId: 1, serviceName: 'main', releaseId: 1, serviceId: 1 },
),
await createService(
{},
{ appId: 1, serviceName: 'aux', releaseId: 1, serviceId: 2 },
),
await createService({ appId: 1, serviceName: 'main' }),
await createService({ appId: 1, serviceName: 'aux' }),
],
networks: [Network.fromComposeObject('test-network', 1, {})],
});
const target = createApp({
services: [
await createService(
{},
{ appId: 1, serviceName: 'main', releaseId: 1, serviceId: 1 },
),
],
services: [await createService({ appId: 1, serviceName: 'main' })],
networks: [Network.fromComposeObject('test-network', 1, {})],
isTarget: true,
});
@ -590,8 +582,8 @@ describe('compose/app', () => {
const current = createApp({
services: [
await createService(
{},
{ serviceName: 'main', state: { status: 'Stopping' } },
{ serviceName: 'main' },
{ state: { status: 'Stopping' } },
),
],
});
@ -608,13 +600,13 @@ describe('compose/app', () => {
const current = createApp({
services: [
await createService(
{},
{ serviceName: 'main', state: { status: 'Dead' } },
{ serviceName: 'main' },
{ state: { status: 'Dead' } },
),
],
});
const target = createApp({
services: [await createService({}, { serviceName: 'main' })],
services: [await createService({ serviceName: 'main' })],
isTarget: true,
});
@ -630,8 +622,8 @@ describe('compose/app', () => {
const current = createApp({
services: [
await createService(
{},
{ serviceName: 'main', state: { status: 'Dead' } },
{ serviceName: 'main' },
{ state: { status: 'Dead' } },
),
],
});
@ -649,13 +641,13 @@ describe('compose/app', () => {
const current = createApp({ services: [] });
const target = createApp({
services: [
await createService({}, { serviceName: 'main', imageId: 123 }),
await createService({ image: 'main-image', serviceName: 'main' }),
],
isTarget: true,
});
const steps = current.nextStepsForAppUpdate(
{ ...defaultContext, ...{ downloading: [123] } },
{ ...defaultContext, ...{ downloading: ['main-image'] } },
target,
);
expectSteps('noop', steps);
@ -665,12 +657,12 @@ describe('compose/app', () => {
it('should emit an updateMetadata step when a service has not changed but the release has', async () => {
const current = createApp({
services: [
await createService({}, { serviceName: 'main', releaseId: 1 }),
await createService({ serviceName: 'main', commit: 'old-release' }),
],
});
const target = createApp({
services: [
await createService({}, { serviceName: 'main', releaseId: 2 }),
await createService({ serviceName: 'main', commit: 'new-release' }),
],
isTarget: true,
});
@ -680,20 +672,20 @@ describe('compose/app', () => {
expect(updateMetadataStep)
.to.have.property('current')
.to.deep.include({ serviceName: 'main', releaseId: 1 });
.to.deep.include({ serviceName: 'main', commit: 'old-release' });
expect(updateMetadataStep)
.to.have.property('target')
.to.deep.include({ serviceName: 'main', releaseId: 2 });
.to.deep.include({ serviceName: 'main', commit: 'new-release' });
});
it('should stop a container which has `running: false` as its target', async () => {
const current = createApp({
services: [await createService({}, { serviceName: 'main' })],
services: [await createService({ serviceName: 'main' })],
});
const target = createApp({
services: [
await createService({ running: false }, { serviceName: 'main' }),
await createService({ running: false, serviceName: 'main' }),
],
isTarget: true,
});
@ -745,30 +737,23 @@ describe('compose/app', () => {
...defaultContext,
...{
availableImages: [
{
appId: 1,
dependent: 0,
imageId: 1,
releaseId: 1,
serviceId: 1,
name: 'main-image',
serviceName: 'main',
},
createImage({ appId: 1, serviceName: 'main', name: 'main-image' }),
],
},
};
const current = createApp({
services: [await createService({}, { appId: 1, serviceName: 'main' })],
services: [await createService({ appId: 1, serviceName: 'main' })],
// Default network was already created
networks: [defaultNetwork],
});
const target = createApp({
services: [
await createService(
{ privileged: true },
{ appId: 1, serviceName: 'main' },
),
await createService({
privileged: true,
appId: 1,
serviceName: 'main',
}),
],
networks: [defaultNetwork],
isTarget: true,
@ -809,36 +794,20 @@ describe('compose/app', () => {
it('should not start a container when it depends on a service which is being installed', async () => {
const availableImages = [
{
appId: 1,
dependent: 0,
imageId: 1,
releaseId: 1,
serviceId: 1,
name: 'main-image',
serviceName: 'main',
},
{
appId: 1,
dependent: 0,
imageId: 2,
releaseId: 1,
serviceId: 2,
name: 'dep-image',
serviceName: 'dep',
},
createImage({ appId: 1, serviceName: 'main', name: 'main-image' }),
createImage({ appId: 1, serviceName: 'dep', name: 'dep-image' }),
];
const contextWithImages = { ...defaultContext, ...{ availableImages } };
const current = createApp({
services: [
await createService(
{ running: false },
{
running: false,
appId: 1,
serviceName: 'dep',
serviceId: 2,
imageId: 2,
},
{
state: {
status: 'Installing',
containerId: 'dep-id',
@ -850,25 +819,15 @@ describe('compose/app', () => {
});
const target = createApp({
services: [
await createService(
{},
{
appId: 1,
serviceName: 'main',
serviceId: 1,
imageId: 1,
state: { dependsOn: ['dep'] },
},
),
await createService(
{},
{
appId: 1,
serviceName: 'dep',
serviceId: 2,
imageId: 2,
},
),
await createService({
appId: 1,
serviceName: 'main',
dependsOn: ['dep'],
}),
await createService({
appId: 1,
serviceName: 'dep',
}),
],
networks: [defaultNetwork],
isTarget: true,
@ -889,16 +848,8 @@ describe('compose/app', () => {
const intermediate = createApp({
services: [
await createService(
{},
{
appId: 1,
serviceName: 'dep',
serviceId: 2,
imageId: 2,
state: {
containerId: 'dep-id',
},
},
{ appId: 1, serviceName: 'dep' },
{ state: { containerId: 'dep-id' } },
),
],
networks: [defaultNetwork],
@ -931,26 +882,18 @@ describe('compose/app', () => {
...defaultContext,
...{
availableImages: [
{
appId: 1,
dependent: 0,
imageId: 1,
releaseId: 1,
serviceId: 1,
name: 'main-image',
serviceName: 'main',
},
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
],
},
};
const current = createApp({
services: [
await createService({ running: false }, { serviceName: 'main' }),
await createService({ running: false, serviceName: 'main' }),
],
networks: [defaultNetwork],
});
const target = createApp({
services: [await createService({}, { serviceName: 'main' })],
services: [await createService({ serviceName: 'main' })],
networks: [defaultNetwork],
isTarget: true,
});
@ -969,15 +912,7 @@ describe('compose/app', () => {
...defaultContext,
...{
availableImages: [
{
appId: 1,
dependent: 0,
imageId: 1,
releaseId: 1,
serviceId: 1,
name: 'main-image',
serviceName: 'main',
},
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
],
},
};
@ -988,31 +923,23 @@ describe('compose/app', () => {
const current = createApp({
services: [
await createService(
{ labels, image: 'main-image' },
{
appId: 1,
serviceName: 'main',
releaseId: 1,
serviceId: 1,
imageId: 1,
},
),
await createService({
labels,
image: 'main-image',
serviceName: 'main',
commit: 'old-release',
}),
],
networks: [defaultNetwork],
});
const target = createApp({
services: [
await createService(
{ labels, image: 'main-image-2' },
{
appId: 1,
serviceName: 'main',
releaseId: 2, // new release
serviceId: 1,
imageId: 2, // new image id
},
),
await createService({
labels,
image: 'main-image-2',
serviceName: 'main',
commit: 'new-release',
}),
],
networks: [defaultNetwork],
isTarget: true,
@ -1049,86 +976,52 @@ describe('compose/app', () => {
const contextWithImages = {
...defaultContext,
...{
downloading: [4], // The depended service image is being downloaded
downloading: ['dep-image-2'], // The depended service image is being downloaded
availableImages: [
{
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
createImage({ appId: 1, name: 'dep-image', serviceName: 'dep' }),
createImage({
appId: 1,
releaseId: 1,
dependent: 0,
name: 'main-image',
imageId: 1,
serviceName: 'main',
serviceId: 1,
},
{
appId: 1,
releaseId: 1,
dependent: 0,
name: 'dep-image',
imageId: 2,
serviceName: 'dep',
serviceId: 2,
},
{
appId: 1,
releaseId: 2,
dependent: 0,
name: 'main-image-2',
imageId: 3,
serviceName: 'main',
serviceId: 1,
},
}),
],
},
};
const current = createApp({
services: [
await createService(
{ image: 'main-image', dependsOn: ['dep'] },
{
appId: 1,
serviceName: 'main',
releaseId: 1,
serviceId: 1,
imageId: 1,
},
),
await createService(
{ image: 'dep-image' },
{
appId: 1,
serviceName: 'dep',
releaseId: 1,
serviceId: 2,
imageId: 2,
},
),
await createService({
image: 'main-image',
dependsOn: ['dep'],
appId: 1,
serviceName: 'main',
commit: 'old-release',
}),
await createService({
image: 'dep-image',
appId: 1,
serviceName: 'dep',
commit: 'old-release',
}),
],
networks: [defaultNetwork],
});
const target = createApp({
services: [
await createService(
{ image: 'main-image-2', dependsOn: ['dep'] },
{
appId: 1,
serviceName: 'main',
releaseId: 2, // new release
serviceId: 1,
imageId: 3, // image has changed
},
),
await createService(
{ image: 'dep-image-2' },
{
appId: 1,
serviceName: 'dep',
releaseId: 2,
serviceId: 2,
imageId: 4,
},
),
await createService({
image: 'main-image-2',
dependsOn: ['dep'],
appId: 1,
serviceName: 'main',
commit: 'new-release',
}),
await createService({
image: 'dep-image-2',
appId: 1,
serviceName: 'dep',
commit: 'new-release',
}),
],
networks: [defaultNetwork],
isTarget: true,
@ -1144,44 +1037,34 @@ describe('compose/app', () => {
...defaultContext,
...{
availableImages: [
{
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
createImage({
appId: 1,
releaseId: 1,
dependent: 0,
name: 'main-image',
imageId: 1,
serviceName: 'main',
serviceId: 1,
},
{
appId: 1,
releaseId: 2,
dependent: 0,
name: 'main-image-2',
imageId: 2,
serviceName: 'main',
serviceId: 1,
},
}),
],
},
};
const current = createApp({
services: [
await createService(
{ image: 'main-image' },
{ serviceName: 'main', releaseId: 1, serviceId: 1, imageId: 1 },
),
await createService({
image: 'main-image',
serviceName: 'main',
commit: 'old-release',
}),
],
networks: [defaultNetwork],
});
const target = createApp({
services: [
await createService(
{ image: 'main-image-2' },
await createService({
image: 'main-image-2',
// new release as target
{ serviceName: 'main', releaseId: 2, serviceId: 1, imageId: 2 },
),
serviceName: 'main',
commit: 'new-release',
}),
],
networks: [defaultNetwork],
isTarget: true,
@ -1238,7 +1121,7 @@ describe('compose/app', () => {
it('should not create a service when a network it depends on is not ready', async () => {
const current = createApp({ networks: [defaultNetwork] });
const target = createApp({
services: [await createService({ networks: ['test'] }, { appId: 1 })],
services: [await createService({ networks: ['test'], appId: 1 })],
networks: [defaultNetwork, Network.fromComposeObject('test', 1, {})],
isTarget: true,
});
@ -1256,50 +1139,30 @@ describe('compose/app', () => {
it('should create several kill steps as long as there are no unmet dependencies', async () => {
const current = createApp({
services: [
await createService(
{},
{
appId: 1,
serviceName: 'one',
releaseId: 1,
serviceId: 1,
imageId: 1,
},
),
await createService(
{},
{
appId: 1,
serviceName: 'two',
releaseId: 1,
serviceId: 2,
imageId: 2,
},
),
await createService(
{},
{
appId: 1,
serviceName: 'three',
releaseId: 1,
serviceId: 3,
imageId: 3,
},
),
await createService({
appId: 1,
serviceName: 'one',
commit: 'old-release',
}),
await createService({
appId: 1,
serviceName: 'two',
commit: 'old-release',
}),
await createService({
appId: 1,
serviceName: 'three',
commit: 'old-release',
}),
],
});
const target = createApp({
services: [
await createService(
{},
{
appId: 1,
serviceName: 'three',
releaseId: 1,
serviceId: 3,
imageId: 3,
},
),
await createService({
appId: 1,
serviceName: 'three',
commit: 'new-release',
}),
],
isTarget: true,
});
@ -1313,7 +1176,7 @@ describe('compose/app', () => {
it('should emit a fetch step when an image has not been downloaded for a service', async () => {
const current = createApp({ services: [] });
const target = createApp({
services: [await createService({}, { serviceName: 'main' })],
services: [await createService({ serviceName: 'main' })],
isTarget: true,
});
@ -1328,13 +1191,13 @@ describe('compose/app', () => {
const contextWithDownloading = {
...defaultContext,
...{
downloading: [1],
downloading: ['image2'],
},
};
const current = createApp({ services: [] });
const target = createApp({
services: [
await createService({}, { serviceName: 'main', imageId: 1 }),
await createService({ image: 'image2', serviceName: 'main' }),
],
isTarget: true,
});

View File

@ -18,24 +18,25 @@ import * as dbHelper from '../../lib/db-helper';
const DEFAULT_NETWORK = Network.fromComposeObject('default', 1, {});
async function createService(
conf = {} as Partial<ServiceComposeConfig>,
{
appId = 1,
serviceName = 'main',
releaseId = 1,
serviceId = 1,
imageId = 1,
state = {} as Partial<Service>,
options = {} as any,
} = {},
commit = 'main-commit',
...conf
} = {} as Partial<ServiceComposeConfig>,
{ state = {} as Partial<Service>, options = {} as any } = {},
) {
const svc = await Service.fromComposeObject(
{
appId,
serviceName,
releaseId,
serviceId,
imageId,
commit,
// db ids should not be used for target state calculation, but images
// are compared using _.isEqual so leaving this here to have image comparisons
// match
serviceId: 1,
imageId: 1,
releaseId: 1,
...conf,
},
options,
@ -48,11 +49,28 @@ async function createService(
return svc;
}
function createImage(svc: Service) {
function createImage(
{
appId = 1,
dependent = 0,
name = 'test-image',
serviceName = 'test',
...extra
} = {} as Partial<Image>,
) {
return {
dockerImageId: svc.config.image,
...imageManager.imageFromService(svc),
};
appId,
dependent,
name,
serviceName,
// db ids should not be used for target state calculation, but images
// are compared using _.isEqual so leaving this here to have image comparisons
// match
imageId: 1,
releaseId: 1,
serviceId: 1,
...extra,
} as Image;
}
function createApps(
@ -110,8 +128,12 @@ function createCurrentState({
services = [] as Service[],
networks = [] as Network[],
volumes = [] as Volume[],
images = services.map((s) => createImage(s)) as Image[],
downloading = [] as number[],
images = services.map((s) => ({
// Infer images from services by default
dockerImageId: s.config.image,
...imageManager.imageFromService(s),
})) as Image[],
downloading = [] as string[],
}) {
const currentApps = createApps({ services, networks, volumes });
@ -183,7 +205,7 @@ describe('compose/application-manager', () => {
it('infers a start step when all that changes is a running state', async () => {
const targetApps = createApps(
{
services: [await createService({ running: true }, { appId: 1 })],
services: [await createService({ running: true, appId: 1 })],
networks: [DEFAULT_NETWORK],
},
true,
@ -194,7 +216,7 @@ describe('compose/application-manager', () => {
downloading,
containerIdsByAppId,
} = createCurrentState({
services: [await createService({ running: false }, { appId: 1 })],
services: [await createService({ running: false, appId: 1 })],
networks: [DEFAULT_NETWORK],
});
@ -251,12 +273,7 @@ describe('compose/application-manager', () => {
it('infers a fetch step when a service has to be updated', async () => {
const targetApps = createApps(
{
services: [
await createService(
{ image: 'image-new' },
{ appId: 1, imageId: 2, options: {} },
),
],
services: [await createService({ image: 'image-new', appId: 1 })],
networks: [DEFAULT_NETWORK],
},
true,
@ -267,7 +284,7 @@ describe('compose/application-manager', () => {
downloading,
containerIdsByAppId,
} = createCurrentState({
services: [await createService({}, { appId: 1, imageId: 1 })],
services: [await createService({ appId: 1 })],
networks: [DEFAULT_NETWORK],
images: [],
});
@ -291,9 +308,7 @@ describe('compose/application-manager', () => {
it('does not infer a fetch step when the download is already in progress', async () => {
const targetApps = createApps(
{
services: [
await createService({ image: 'image-new' }, { appId: 1, imageId: 2 }),
],
services: [await createService({ image: 'image-new', appId: 1 })],
networks: [DEFAULT_NETWORK],
},
true,
@ -304,9 +319,9 @@ describe('compose/application-manager', () => {
downloading,
containerIdsByAppId,
} = createCurrentState({
services: [await createService({}, { appId: 1, imageId: 1 })],
services: [await createService({ appId: 1 })],
networks: [DEFAULT_NETWORK],
downloading: [2],
downloading: ['image-new'],
});
const [noopStep, ...nextSteps] = await applicationManager.inferNextSteps(
@ -330,10 +345,12 @@ describe('compose/application-manager', () => {
const targetApps = createApps(
{
services: [
await createService(
{ image: 'image-new', labels },
{ appId: 1, imageId: 2 },
),
await createService({
image: 'image-new',
labels,
appId: 1,
commit: 'new-release',
}),
],
networks: [DEFAULT_NETWORK],
},
@ -346,10 +363,12 @@ describe('compose/application-manager', () => {
containerIdsByAppId,
} = createCurrentState({
services: [
await createService(
{ image: 'image-old', labels },
{ appId: 1, imageId: 1 },
),
await createService({
image: 'image-old',
labels,
appId: 1,
commit: 'old-release',
}),
],
networks: [DEFAULT_NETWORK],
});
@ -374,26 +393,19 @@ describe('compose/application-manager', () => {
const targetApps = createApps(
{
services: [
await createService(
{ image: 'main-image', dependsOn: ['dep'] },
{
appId: 1,
imageId: 3,
serviceId: 1,
serviceName: 'main',
releaseId: 2,
},
),
await createService(
{ image: 'dep-image' },
{
appId: 1,
imageId: 4,
serviceId: 2,
serviceName: 'dep',
releaseId: 2,
},
),
await createService({
image: 'main-image',
dependsOn: ['dep'],
appId: 1,
commit: 'new-release',
serviceName: 'main',
}),
await createService({
image: 'dep-image',
appId: 1,
commit: 'new-release',
serviceName: 'dep',
}),
],
networks: [DEFAULT_NETWORK],
},
@ -406,28 +418,27 @@ describe('compose/application-manager', () => {
containerIdsByAppId,
} = createCurrentState({
services: [
await createService(
{ dependsOn: ['dep'] },
{ appId: 1, imageId: 1, serviceId: 1, serviceName: 'main' },
),
await createService(
{},
{ appId: 1, imageId: 2, serviceId: 2, serviceName: 'dep' },
),
await createService({
dependsOn: ['dep'],
appId: 1,
commit: 'old-release',
serviceName: 'main',
}),
await createService({
appId: 1,
commit: 'old-release',
serviceName: 'dep',
}),
],
networks: [DEFAULT_NETWORK],
downloading: [4], // dep-image is still being downloaded
downloading: ['dep-image'], // dep-image is still being downloaded
images: [
// main-image was already downloaded
{
createImage({
appId: 1,
releaseId: 2,
name: 'main-image',
imageId: 3,
serviceName: 'main',
serviceId: 1,
dependent: 0,
},
}),
],
});
@ -449,26 +460,19 @@ describe('compose/application-manager', () => {
const targetApps = createApps(
{
services: [
await createService(
{ image: 'main-image', dependsOn: ['dep'] },
{
appId: 1,
imageId: 3,
serviceId: 1,
serviceName: 'main',
releaseId: 2,
},
),
await createService(
{ image: 'dep-image' },
{
appId: 1,
imageId: 4,
serviceId: 2,
serviceName: 'dep',
releaseId: 2,
},
),
await createService({
image: 'main-image',
dependsOn: ['dep'],
appId: 1,
commit: 'new-release',
serviceName: 'main',
}),
await createService({
image: 'dep-image',
appId: 1,
commit: 'new-release',
serviceName: 'dep',
}),
],
networks: [DEFAULT_NETWORK],
},
@ -482,36 +486,31 @@ describe('compose/application-manager', () => {
containerIdsByAppId,
} = createCurrentState({
services: [
await createService(
{ dependsOn: ['dep'] },
{ appId: 1, imageId: 1, serviceId: 1, serviceName: 'main' },
),
await createService(
{},
{ appId: 1, imageId: 2, serviceId: 2, serviceName: 'dep' },
),
await createService({
dependsOn: ['dep'],
appId: 1,
commit: 'old-release',
serviceName: 'main',
}),
await createService({
appId: 1,
commit: 'old-release',
serviceName: 'dep',
}),
],
networks: [DEFAULT_NETWORK],
images: [
// Both images have been downloaded
{
createImage({
appId: 1,
releaseId: 2,
name: 'main-image',
imageId: 3,
serviceName: 'main',
serviceId: 1,
dependent: 0,
},
{
}),
createImage({
appId: 1,
releaseId: 2,
name: 'dep-image',
imageId: 4,
serviceName: 'dep',
serviceId: 2,
dependent: 0,
},
}),
],
});
@ -542,22 +541,17 @@ describe('compose/application-manager', () => {
const targetApps = createApps(
{
services: [
await createService(
{ image: 'main-image', dependsOn: ['dep'] },
{
imageId: 1,
serviceId: 1,
serviceName: 'main',
},
),
await createService(
{ image: 'dep-image' },
{
imageId: 2,
serviceId: 2,
serviceName: 'dep',
},
),
await createService({
image: 'main-image',
dependsOn: ['dep'],
serviceName: 'main',
commit: 'new-release',
}),
await createService({
image: 'dep-image',
serviceName: 'dep',
commit: 'new-release',
}),
],
networks: [DEFAULT_NETWORK],
},
@ -574,24 +568,16 @@ describe('compose/application-manager', () => {
networks: [DEFAULT_NETWORK],
images: [
// Both images have been downloaded
{
createImage({
appId: 1,
releaseId: 1,
name: 'main-image',
imageId: 1,
serviceName: 'main',
serviceId: 1,
dependent: 0,
},
{
}),
createImage({
appId: 1,
releaseId: 1,
name: 'dep-image',
imageId: 2,
serviceName: 'dep',
serviceId: 2,
dependent: 0,
},
}),
],
});
@ -619,22 +605,17 @@ describe('compose/application-manager', () => {
const targetApps = createApps(
{
services: [
await createService(
{ image: 'main-image', dependsOn: ['dep'] },
{
imageId: 1,
serviceId: 1,
serviceName: 'main',
},
),
await createService(
{ image: 'dep-image' },
{
imageId: 2,
serviceId: 2,
serviceName: 'dep',
},
),
await createService({
image: 'main-image',
dependsOn: ['dep'],
serviceName: 'main',
commit: 'new-release',
}),
await createService({
image: 'dep-image',
serviceName: 'dep',
commit: 'new-release',
}),
],
networks: [DEFAULT_NETWORK],
},
@ -648,36 +629,25 @@ describe('compose/application-manager', () => {
containerIdsByAppId,
} = createCurrentState({
services: [
await createService(
{ image: 'dep-image' },
{
imageId: 2,
serviceId: 2,
serviceName: 'dep',
},
),
await createService({
image: 'dep-image',
serviceName: 'dep',
commit: 'new-release',
}),
],
networks: [DEFAULT_NETWORK],
images: [
// Both images have been downloaded
{
createImage({
appId: 1,
releaseId: 1,
name: 'main-image',
imageId: 1,
serviceName: 'main',
serviceId: 1,
dependent: 0,
},
{
}),
createImage({
appId: 1,
releaseId: 1,
name: 'dep-image',
imageId: 2,
serviceName: 'dep',
serviceId: 2,
dependent: 0,
},
}),
],
});
@ -714,27 +684,15 @@ describe('compose/application-manager', () => {
downloading,
containerIdsByAppId,
} = createCurrentState({
services: [
await createService(
{},
{
appId: 5,
serviceName: 'old-service',
},
),
],
services: [await createService({ appId: 5, serviceName: 'old-service' })],
networks: [DEFAULT_NETWORK],
images: [
// Both images have been downloaded
{
// Image has been downloaded
createImage({
appId: 1,
releaseId: 1,
name: 'main-image',
imageId: 1,
serviceName: 'main',
serviceId: 1,
dependent: 0,
},
}),
],
});
@ -822,7 +780,7 @@ describe('compose/application-manager', () => {
(networkManager.supervisorNetworkReady as sinon.SinonStub).resolves(false);
const targetApps = createApps(
{ services: [await createService({})], networks: [DEFAULT_NETWORK] },
{ services: [await createService()], networks: [DEFAULT_NETWORK] },
true,
);
const {
@ -955,26 +913,18 @@ describe('compose/application-manager', () => {
networks: [DEFAULT_NETWORK],
images: [
// An image for a service that no longer exists
{
createImage({
name: 'old-image',
appId: 5,
serviceId: 5,
serviceName: 'old-service',
imageId: 5,
dependent: 0,
releaseId: 5,
dockerImageId: 'sha256:aaaa',
},
{
}),
createImage({
name: 'main-image',
appId: 1,
serviceId: 1,
serviceName: 'main',
imageId: 1,
dependent: 0,
releaseId: 1,
dockerImageId: 'sha256:bbbb',
},
}),
],
});
@ -1021,26 +971,18 @@ describe('compose/application-manager', () => {
networks: [DEFAULT_NETWORK],
images: [
// An image for a service that no longer exists
{
createImage({
name: 'old-image',
appId: 5,
serviceId: 5,
serviceName: 'old-service',
imageId: 5,
dependent: 0,
releaseId: 5,
dockerImageId: 'sha256:aaaa',
},
{
}),
createImage({
name: 'main-image',
appId: 1,
serviceId: 1,
serviceName: 'main',
imageId: 1,
dependent: 0,
releaseId: 1,
dockerImageId: 'sha256:bbbb',
},
}),
],
});
@ -1109,14 +1051,18 @@ describe('compose/application-manager', () => {
const targetApps = createApps(
{
services: [
await createService(
{ running: true, image: 'main-image-1' },
{ appId: 1, serviceId: 1, imageId: 1 },
),
await createService(
{ running: true, image: 'main-image-2' },
{ appId: 2, serviceId: 2, imageId: 2 },
),
await createService({
running: true,
image: 'main-image-1',
appId: 1,
commit: 'commit-for-app-1',
}),
await createService({
running: true,
image: 'main-image-2',
appId: 2,
commit: 'commit-for-app-2',
}),
],
networks: [
// Default networks for two apps
@ -1139,24 +1085,16 @@ describe('compose/application-manager', () => {
Network.fromComposeObject('default', 2, {}),
],
images: [
{
createImage({
name: 'main-image-1',
appId: 1,
serviceId: 1,
serviceName: 'main',
imageId: 1,
dependent: 0,
releaseId: 1,
},
{
}),
createImage({
name: 'main-image-2',
appId: 2,
serviceId: 2,
serviceName: 'main',
imageId: 2,
dependent: 0,
releaseId: 1,
},
}),
],
});