mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-05-25 03:44:20 +00:00
This reduces circular dependencies from 250 to 80 by ensuring that modules that only require types do not import the full module with all its dependencies. Change-type: patch
162 lines
4.3 KiB
TypeScript
162 lines
4.3 KiB
TypeScript
import type Docker from 'dockerode';
|
|
import isEqual = require('lodash/isEqual');
|
|
import omitBy = require('lodash/omitBy');
|
|
|
|
import * as constants from '../lib/constants';
|
|
import { docker } from '../lib/docker-utils';
|
|
import { InternalInconsistencyError } from '../lib/errors';
|
|
import * as LogTypes from '../lib/log-types';
|
|
import type { LabelObject } from '../types';
|
|
import * as logger from '../logger';
|
|
import * as ComposeUtils from './utils';
|
|
|
|
import type {
|
|
Volume as VolumeIface,
|
|
VolumeConfig,
|
|
ComposeVolumeConfig,
|
|
} from './types';
|
|
|
|
export type Volume = VolumeIface;
|
|
|
|
class VolumeImpl implements Volume {
|
|
private constructor(
|
|
public name: string,
|
|
public appId: number,
|
|
public appUuid: string,
|
|
public config: VolumeConfig,
|
|
) {}
|
|
|
|
public static fromDockerVolume(inspect: Docker.VolumeInspectInfo): Volume {
|
|
// Convert the docker inspect to the config
|
|
const config: VolumeConfig = {
|
|
labels: inspect.Labels || {},
|
|
driver: inspect.Driver,
|
|
driverOpts: inspect.Options || {},
|
|
};
|
|
|
|
// Detect the name and appId from the inspect data
|
|
const { name, appId } = this.deconstructDockerName(inspect.Name);
|
|
const appUuid = config.labels['io.balena.app-uuid'];
|
|
|
|
return new Volume(name, appId, appUuid, config);
|
|
}
|
|
|
|
public static fromComposeObject(
|
|
name: string,
|
|
appId: number,
|
|
appUuid: string,
|
|
config = {} as Partial<ComposeVolumeConfig>,
|
|
) {
|
|
const filledConfig: VolumeConfig = {
|
|
driverOpts: config.driver_opts || {},
|
|
driver: config.driver || 'local',
|
|
labels: {
|
|
// We only need to assign the labels here, as when we
|
|
// get it from the daemon, they should already be there
|
|
...ComposeUtils.normalizeLabels(config.labels || {}),
|
|
...constants.defaultVolumeLabels,
|
|
|
|
// the app uuid will always be in the target state, the
|
|
// only reason this is done this way is to be compatible
|
|
// with loading a volume from backup (see lib/migration)
|
|
...(appUuid && { 'io.balena.app-uuid': appUuid }),
|
|
},
|
|
};
|
|
|
|
return new Volume(name, appId, appUuid, filledConfig);
|
|
}
|
|
|
|
public toComposeObject(): ComposeVolumeConfig {
|
|
return {
|
|
driver: this.config.driver,
|
|
driver_opts: this.config.driverOpts!,
|
|
labels: this.config.labels,
|
|
};
|
|
}
|
|
|
|
public isEqualConfig(volume: Volume): boolean {
|
|
return (
|
|
isEqual(this.config.driver, volume.config.driver) &&
|
|
isEqual(this.config.driverOpts, volume.config.driverOpts) &&
|
|
isEqual(
|
|
Volume.omitSupervisorLabels(this.config.labels),
|
|
Volume.omitSupervisorLabels(volume.config.labels),
|
|
)
|
|
);
|
|
}
|
|
|
|
public async create(): Promise<void> {
|
|
logger.logSystemEvent(LogTypes.createVolume, {
|
|
volume: { name: this.name },
|
|
});
|
|
await docker.createVolume({
|
|
Name: Volume.generateDockerName(this.appId, this.name),
|
|
Labels: this.config.labels,
|
|
Driver: this.config.driver,
|
|
DriverOpts: this.config.driverOpts!,
|
|
});
|
|
}
|
|
|
|
public async remove(): Promise<void> {
|
|
logger.logSystemEvent(LogTypes.removeVolume, {
|
|
volume: { name: this.name },
|
|
});
|
|
|
|
try {
|
|
await docker
|
|
.getVolume(Volume.generateDockerName(this.appId, this.name))
|
|
.remove();
|
|
} catch (e) {
|
|
logger.logSystemEvent(LogTypes.removeVolumeError, {
|
|
volume: { name: this.name, appId: this.appId },
|
|
error: e,
|
|
});
|
|
}
|
|
}
|
|
|
|
public static generateDockerName(appId: number, name: string) {
|
|
return `${appId}_${name}`;
|
|
}
|
|
|
|
private static deconstructDockerName(name: string): {
|
|
name: string;
|
|
appId: number;
|
|
} {
|
|
const match = name.match(/(\d+)_(\S+)/);
|
|
if (match == null) {
|
|
throw new InternalInconsistencyError(
|
|
`Could not detect volume data from docker name: ${name}`,
|
|
);
|
|
}
|
|
|
|
const appId = parseInt(match[1], 10);
|
|
if (isNaN(appId)) {
|
|
throw new InternalInconsistencyError(
|
|
`Could not detect application id from docker name: ${match[1]}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
appId,
|
|
name: match[2],
|
|
};
|
|
}
|
|
|
|
private static omitSupervisorLabels(labels: LabelObject): LabelObject {
|
|
// TODO: Export these to a constant
|
|
return omitBy(
|
|
labels,
|
|
(_v, k) =>
|
|
k === 'io.resin.supervised' ||
|
|
k === 'io.balena.supervised' ||
|
|
// TODO: we need to omit the app-uuid label
|
|
// in the comparison or else the supervisor will try to recreate
|
|
// the volume, which won't fail but won't have any effect on the volume
|
|
// either, leading to a supervisor target state apply loop
|
|
k === 'io.balena.app-uuid',
|
|
);
|
|
}
|
|
}
|
|
|
|
export const Volume = VolumeImpl;
|