Allow application manager to match apps between environments

If an app with the same app uuid exists between environments, the
supervisor will match the apps by uuid to prevent stopping the running
app
This commit is contained in:
Felipe Lalanne 2021-08-27 21:46:01 +00:00
parent 5c5483dd3d
commit e9af9d8e83

View File

@ -27,6 +27,8 @@ import { getExecutors, CompositionStepT } from './composition-steps';
import * as commitStore from './commit';
import Service from './service';
import Network from './network';
import Volume from './volume';
import { createV1Api } from '../device-api/v1';
import { createV2Api } from '../device-api/v2';
@ -351,35 +353,154 @@ export async function stopAll({ force = false, skipLock = false } = {}) {
);
}
// 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
// still need to be able to work on these and perform various state changes. To
// do this we try to use the UUID to group the components, and if that isn't
// available we revert to using the appIds instead
export async function getCurrentApps(): Promise<InstancedAppState> {
const volumes = _.groupBy(await volumeManager.getAll(), 'appId');
const networks = _.groupBy(await networkManager.getAll(), 'appId');
const services = _.groupBy(await serviceManager.getAll(), 'appId');
const allAppIds = _.union(
Object.keys(volumes),
Object.keys(networks),
Object.keys(services),
).map((i) => parseInt(i, 10));
const componentGroups = groupComponents(
await serviceManager.getAll(),
await networkManager.getAll(),
await volumeManager.getAll(),
);
const apps: InstancedAppState = {};
for (const appId of allAppIds) {
for (const strAppId of Object.keys(componentGroups)) {
const appId = parseInt(strAppId, 10);
// TODO: get commit and release version from container
const commit = await commitStore.getCommitForApp(appId);
apps[appId] = new App(
{
appId,
services: services[appId] ?? [],
networks: _.keyBy(networks[appId], 'name'),
volumes: _.keyBy(volumes[appId], 'name'),
commit,
},
false,
);
const components = componentGroups[appId];
// fetch the correct uuid from any component within the appId
const uuid = [
components.services[0]?.appUuid,
components.volumes[0]?.appUuid,
components.networks[0]?.appUuid,
]
.filter((u) => !!u)
.shift()!;
// If we don't have any components for this app, ignore it (this can
// actually happen when moving between backends but maintaining UUIDs)
if (
!_.isEmpty(components.services) ||
!_.isEmpty(components.volumes) ||
!_.isEmpty(components.networks)
) {
apps[appId] = new App(
{
appId,
appUuid: uuid,
commit,
services: componentGroups[appId].services,
networks: _.keyBy(componentGroups[appId].networks, 'name'),
volumes: _.keyBy(componentGroups[appId].volumes, 'name'),
},
false,
);
}
}
return apps;
}
type AppGroup = {
[appId: number]: {
services: Service[];
volumes: Volume[];
networks: Network[];
};
};
function groupComponents(
services: Service[],
networks: Network[],
volumes: Volume[],
): AppGroup {
const grouping: AppGroup = {};
const everyComponent: [{ appUuid?: string; appId: number }] = [
...services,
...networks,
...volumes,
] as any;
const allUuids: string[] = [];
const allAppIds: number[] = [];
everyComponent.forEach(({ appId, appUuid }) => {
// Pre-populate the groupings
grouping[appId] = {
services: [],
networks: [],
volumes: [],
};
// Save all the uuids for later
if (appUuid != null) {
allUuids.push(appUuid);
}
allAppIds.push(appId);
});
// First we try to group everything by it's uuid, but if any component does
// not have a uuid, we fall back to the old appId style
if (everyComponent.length === allUuids.length) {
const uuidGroups: { [uuid: string]: AppGroup[0] } = {};
new Set(allUuids).forEach((uuid) => {
const uuidServices = services.filter(
({ appUuid: sUuid }) => uuid === sUuid,
);
const uuidVolumes = volumes.filter(
({ appUuid: vUuid }) => uuid === vUuid,
);
const uuidNetworks = networks.filter(
({ appUuid: nUuid }) => uuid === nUuid,
);
uuidGroups[uuid] = {
services: uuidServices,
networks: uuidNetworks,
volumes: uuidVolumes,
};
});
for (const uuid of Object.keys(uuidGroups)) {
// There's a chance that the uuid and the appId is different, and this
// is fine. Unfortunately we have no way of knowing which is the "real"
// appId (that is the app id which relates to the currently joined
// backend) so we instead just choose the first and add everything to that
const appId =
uuidGroups[uuid].services[0]?.appId ||
uuidGroups[uuid].networks[0]?.appId ||
uuidGroups[uuid].volumes[0]?.appId;
grouping[appId] = uuidGroups[uuid];
}
} else {
// Otherwise group them by appId and let the state engine match them later.
// This will only happen once, as every target state going forward will
// contain UUIDs, we just need to handle the initial upgrade
const appSvcs = _.groupBy(services, 'appId');
const appVols = _.groupBy(volumes, 'appId');
const appNets = _.groupBy(networks, 'appId');
_.uniq(allAppIds).forEach((appId) => {
grouping[appId].services = grouping[appId].services.concat(
appSvcs[appId] || [],
);
grouping[appId].networks = grouping[appId].networks.concat(
appNets[appId] || [],
);
grouping[appId].volumes = grouping[appId].volumes.concat(
appVols[appId] || [],
);
});
}
return grouping;
}
function killServicesUsingApi(current: InstancedAppState): CompositionStep[] {
const steps: CompositionStep[] = [];
_.each(current, (app) => {