Split compose types into interface and implementation

This splits `App`, `Network`, `Service` and `Volume` which used to be
defined as classes into an interface and a class implementation that is
not exported. This will allow to work with just the types in some cases
and prevent circular dependencies when importing.

Change-type: patch
This commit is contained in:
Felipe Lalanne 2024-05-15 13:44:05 -04:00
parent 435a363716
commit 94de4006a0
26 changed files with 143 additions and 60 deletions

View File

@ -2,9 +2,9 @@ import _ from 'lodash';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import type { ImageInspectInfo } from 'dockerode'; import type { ImageInspectInfo } from 'dockerode';
import Network from './network'; import { Network } from './network';
import Volume from './volume'; import { Volume } from './volume';
import Service from './service'; import { Service } from './service';
import * as imageManager from './images'; import * as imageManager from './images';
import type { Image } from './images'; import type { Image } from './images';
import type { import type {
@ -57,7 +57,27 @@ export interface AppsToLockMap {
[appId: number]: Set<string>; [appId: number]: Set<string>;
} }
export class App { export interface App {
appId: number;
appUuid?: string;
// When setting up an application from current state, these values are not available
appName?: string;
commit?: string;
source?: string;
isHost?: boolean;
// Services are stored as an array, as at any one time we could have more than one
// service for a single service ID running (for example handover)
services: Service[];
networks: Network[];
volumes: Volume[];
nextStepsForAppUpdate(state: UpdateState, target: App): CompositionStep[];
stepsToRemoveApp(
state: Omit<UpdateState, 'availableImages'> & { keepVolumes: boolean },
): CompositionStep[];
}
class AppImpl implements App {
public appId: number; public appId: number;
public appUuid?: string; public appUuid?: string;
// When setting up an application from current state, these values are not available // When setting up an application from current state, these values are not available
@ -1027,7 +1047,7 @@ export class App {
}), }),
); );
return new App( return new AppImpl(
{ {
appId: app.appId, appId: app.appId,
appUuid: app.uuid, appUuid: app.uuid,
@ -1044,4 +1064,4 @@ export class App {
} }
} }
export default App; export const App = AppImpl;

View File

@ -20,16 +20,16 @@ import {
import { getServicesLockedByAppId, LocksTakenMap } from '../lib/update-lock'; import { getServicesLockedByAppId, LocksTakenMap } from '../lib/update-lock';
import { checkTruthy } from '../lib/validation'; import { checkTruthy } from '../lib/validation';
import App from './app'; import { App } from './app';
import type { UpdateState } from './app'; import type { UpdateState } from './app';
import * as volumeManager from './volume-manager'; import * as volumeManager from './volume-manager';
import * as networkManager from './network-manager'; import * as networkManager from './network-manager';
import * as serviceManager from './service-manager'; import * as serviceManager from './service-manager';
import * as imageManager from './images'; import * as imageManager from './images';
import * as commitStore from './commit'; import * as commitStore from './commit';
import type Service from './service'; import type { Service } from './service';
import type Network from './network'; import type { Network } from './network';
import type Volume from './volume'; import type { Volume } from './volume';
import { generateStep, getExecutors } from './composition-steps'; import { generateStep, getExecutors } from './composition-steps';
import type { import type {

View File

@ -1,12 +1,12 @@
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';
import type Network from './network'; import type { Network } from './network';
import type Service from './service'; import type { Service } from './service';
import * as serviceManager from './service-manager'; import * as serviceManager from './service-manager';
import * as networkManager from './network-manager'; import * as networkManager from './network-manager';
import * as volumeManager from './volume-manager'; import * as volumeManager from './volume-manager';
import type Volume from './volume'; import type { Volume } from './volume';
import * as commitStore from './commit'; 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';

View File

@ -11,12 +11,27 @@ import type {
ComposeNetworkConfig, ComposeNetworkConfig,
NetworkConfig, NetworkConfig,
NetworkInspectInfo, NetworkInspectInfo,
} from './types/network'; } from './types';
import { InvalidNetworkNameError } from './errors'; import { InvalidNetworkNameError } from './errors';
import { InternalInconsistencyError } from '../lib/errors'; import { InternalInconsistencyError } from '../lib/errors';
export class Network { export interface Network {
appId: number;
appUuid?: string;
name: string;
config: NetworkConfig;
isEqualConfig(network: Network): boolean;
create(): Promise<void>;
remove(): Promise<void>;
toDockerConfig(): dockerode.NetworkCreateOptions & {
ConfigOnly: boolean;
};
toComposeObject(): ComposeNetworkConfig;
}
class NetworkImpl implements Network {
public appId: number; public appId: number;
public appUuid?: string; public appUuid?: string;
public name: string; public name: string;
@ -303,4 +318,4 @@ export class Network {
} }
} }
export default Network; export const Network = NetworkImpl;

View File

@ -23,7 +23,7 @@ import type {
ConfigMap, ConfigMap,
DeviceMetadata, DeviceMetadata,
DockerDevice, DockerDevice,
} from './types/service'; } from './types';
import { import {
ShortMount, ShortMount,
ShortBind, ShortBind,
@ -34,7 +34,7 @@ import {
LongBind, LongBind,
LongAnonymousVolume, LongAnonymousVolume,
LongNamedVolume, LongNamedVolume,
} from './types/service'; } from './types';
const SERVICE_NETWORK_MODE_REGEX = /service:\s*(.+)/; const SERVICE_NETWORK_MODE_REGEX = /service:\s*(.+)/;
const CONTAINER_NETWORK_MODE_REGEX = /container:\s*(.+)/; const CONTAINER_NETWORK_MODE_REGEX = /container:\s*(.+)/;
@ -52,7 +52,51 @@ export type ServiceStatus =
| 'removing' | 'removing'
| 'exited'; | 'exited';
export class Service { export interface Service {
appId: number;
appUuid?: string;
imageId: number;
config: ServiceConfig;
serviceName: string;
commit: string;
releaseId: number;
serviceId: number;
imageName: string | null;
containerId: string | null;
exitErrorMessage: string | null;
dependsOn: string[] | null;
dockerImageId: string | null;
// This looks weird, and it is. The lowercase statuses come from Docker,
// except the dashboard takes these values and displays them on the dashboard.
// What we should be doin is defining these container statuses, and have the
// dashboard make these human readable instead. Until that happens we have
// this halfways state of some captalised statuses, and others coming directly
// from docker
status: ServiceStatus;
createdAt: Date | null;
hasNetwork(networkName: string): boolean;
hasVolume(volumeName: string): boolean;
isEqualExceptForRunningState(
service: Service,
currentContainerIds: Dictionary<string>,
): boolean;
isEqualConfig(
service: Service,
currentContainerIds: Dictionary<string>,
): boolean;
hasNetworkMode(networkName: string): boolean;
extraNetworksToJoin(): ServiceConfig['networks'];
toDockerContainer(opts: {
deviceName: string;
containerIds: Dictionary<string>;
}): Dockerode.ContainerCreateOptions;
handoverCompleteFullPathsOnHost(): string[];
}
class ServiceImpl implements Service {
public appId: number; public appId: number;
public appUuid?: string; public appUuid?: string;
public imageId: number; public imageId: number;
@ -64,17 +108,8 @@ export class Service {
public imageName: string | null; public imageName: string | null;
public containerId: string | null; public containerId: string | null;
public exitErrorMessage: string | null; public exitErrorMessage: string | null;
public dependsOn: string[] | null; public dependsOn: string[] | null;
public dockerImageId: string | null; public dockerImageId: string | null;
// This looks weird, and it is. The lowercase statuses come from Docker,
// except the dashboard takes these values and displays them on the dashboard.
// What we should be doin is defining these container statuses, and have the
// dashboard make these human readable instead. Until that happens we have
// this halfways state of some captalised statuses, and others coming directly
// from docker
public status: ServiceStatus; public status: ServiceStatus;
public createdAt: Date | null; public createdAt: Date | null;
@ -95,7 +130,7 @@ export class Service {
'dnsSearch', 'dnsSearch',
]; ];
public static allConfigArrayFields: ServiceConfigArrayField[] = public static allConfigArrayFields: ServiceConfigArrayField[] =
Service.configArrayFields.concat(Service.orderedConfigArrayFields); ServiceImpl.configArrayFields.concat(ServiceImpl.orderedConfigArrayFields);
// A list of fields to ignore when comparing container configuration // A list of fields to ignore when comparing container configuration
private static omitFields = [ private static omitFields = [
@ -109,7 +144,7 @@ export class Service {
// These fields are special case, due to network_mode:service:<service> // These fields are special case, due to network_mode:service:<service>
'networkMode', 'networkMode',
'hostname', 'hostname',
].concat(Service.allConfigArrayFields); ].concat(ServiceImpl.allConfigArrayFields);
private constructor() { private constructor() {
/* do not allow instancing a service object with `new` */ /* do not allow instancing a service object with `new` */
@ -1170,4 +1205,4 @@ export class Service {
} }
} }
export default Service; export const Service = ServiceImpl;

View File

@ -0,0 +1,2 @@
export * from './service';
export * from './network';

View File

@ -1,5 +1,5 @@
import * as imageManager from './images'; import * as imageManager from './images';
import type Service from './service'; import type { Service } from './service';
import type { CompositionStep } from './composition-steps'; import type { CompositionStep } from './composition-steps';
import { generateStep } from './composition-steps'; import { generateStep } from './composition-steps';
import type { AppsToLockMap } from './app'; import type { AppsToLockMap } from './app';

View File

@ -16,7 +16,7 @@ import type {
ServiceHealthcheck, ServiceHealthcheck,
LongDefinition, LongDefinition,
LongBind, LongBind,
} from './types/service'; } from './types';
import log from '../lib/supervisor-console'; import log from '../lib/supervisor-console';

View File

@ -11,7 +11,7 @@ import log from '../lib/supervisor-console';
import * as logger from '../logger'; import * as logger from '../logger';
import { ResourceRecreationAttemptError } from './errors'; import { ResourceRecreationAttemptError } from './errors';
import type { VolumeConfig } from './volume'; import type { VolumeConfig } from './volume';
import Volume from './volume'; import { Volume } from './volume';
export interface VolumeNameOpts { export interface VolumeNameOpts {
name: string; name: string;

View File

@ -22,7 +22,18 @@ export interface ComposeVolumeConfig {
labels: LabelObject; labels: LabelObject;
} }
export class Volume { export interface Volume {
name: string;
appId: number;
appUuid: string;
config: VolumeConfig;
isEqualConfig(volume: Volume): boolean;
create(): Promise<void>;
remove(): Promise<void>;
}
class VolumeImpl implements Volume {
private constructor( private constructor(
public name: string, public name: string,
public appId: number, public appId: number,
@ -162,4 +173,4 @@ export class Volume {
} }
} }
export default Volume; export const Volume = VolumeImpl;

View File

@ -11,7 +11,7 @@ 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 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';

View File

@ -7,7 +7,7 @@ import * as apiBinder from '../api-binder';
import * as applicationManager from '../compose/application-manager'; import * as applicationManager from '../compose/application-manager';
import type { CompositionStepAction } from '../compose/composition-steps'; import type { CompositionStepAction } from '../compose/composition-steps';
import type { Service } from '../compose/service'; import type { Service } from '../compose/service';
import Volume from '../compose/volume'; import { Volume } from '../compose/volume';
import * as commitStore from '../compose/commit'; import * as commitStore from '../compose/commit';
import * as config from '../config'; import * as config from '../config';
import * as db from '../db'; import * as db from '../db';

View File

@ -4,7 +4,7 @@ import type * as db from '../db';
import * as targetStateCache from './target-state-cache'; import * as targetStateCache from './target-state-cache';
import type { DatabaseApp, DatabaseService } from './target-state-cache'; import type { DatabaseApp, DatabaseService } from './target-state-cache';
import App from '../compose/app'; import { App } from '../compose/app';
import * as images from '../compose/images'; import * as images from '../compose/images';
import type { import type {

View File

@ -14,7 +14,7 @@ import {
import { docker } from './docker-utils'; import { docker } from './docker-utils';
import { log } from './supervisor-console'; import { log } from './supervisor-console';
import { pathOnData } from './host-utils'; import { pathOnData } from './host-utils';
import type Volume from '../compose/volume'; import type { Volume } from '../compose/volume';
import * as logger from '../logger'; import * as logger from '../logger';
import type { import type {
DatabaseApp, DatabaseApp,

View File

@ -1,7 +1,7 @@
import * as t from 'io-ts'; import * as t from 'io-ts';
// TODO: move all these exported types to ../compose/types // TODO: move all these exported types to ../compose/types
import type { ComposeNetworkConfig } from '../compose/types/network'; import type { ComposeNetworkConfig } from '../compose/types';
import type { ComposeVolumeConfig } from '../compose/volume'; import type { ComposeVolumeConfig } from '../compose/volume';
import type { ContractObject } from '../lib/contracts'; import type { ContractObject } from '../lib/contracts';
@ -16,7 +16,7 @@ import {
nonEmptyRecord, nonEmptyRecord,
} from './basic'; } from './basic';
import type App from '../compose/app'; import type { App } from '../compose/app';
export type DeviceLegacyReport = Partial<{ export type DeviceLegacyReport = Partial<{
api_port: number; api_port: number;

View File

@ -4,9 +4,9 @@ import Docker from 'dockerode';
import * as applicationManager from '~/src/compose/application-manager'; import * as applicationManager from '~/src/compose/application-manager';
import * as imageManager from '~/src/compose/images'; import * as imageManager from '~/src/compose/images';
import * as serviceManager from '~/src/compose/service-manager'; import * as serviceManager from '~/src/compose/service-manager';
import Network from '~/src/compose/network'; import { Network } from '~/src/compose/network';
import * as networkManager from '~/src/compose/network-manager'; import * as networkManager from '~/src/compose/network-manager';
import Volume from '~/src/compose/volume'; import { Volume } from '~/src/compose/volume';
import * as config from '~/src/config'; import * as config from '~/src/config';
import { LocksTakenMap } from '~/lib/update-lock'; import { LocksTakenMap } from '~/lib/update-lock';
import { createDockerImage } from '~/test-lib/docker-helper'; import { createDockerImage } from '~/test-lib/docker-helper';

View File

@ -1,6 +1,6 @@
import { expect } from 'chai'; import { expect } from 'chai';
import Service from '~/src/compose/service'; import { Service } from '~/src/compose/service';
import * as deviceApi from '~/src/device-api'; import * as deviceApi from '~/src/device-api';
describe('compose/service: integration tests', () => { describe('compose/service: integration tests', () => {

View File

@ -2,7 +2,7 @@ import { expect } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as volumeManager from '~/src/compose/volume-manager'; import * as volumeManager from '~/src/compose/volume-manager';
import Volume from '~/src/compose/volume'; import { Volume } from '~/src/compose/volume';
import { createDockerImage } from '~/test-lib/docker-helper'; import { createDockerImage } from '~/test-lib/docker-helper';
import Docker from 'dockerode'; import Docker from 'dockerode';

View File

@ -1,7 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import type { SinonStub } from 'sinon'; import type { SinonStub } from 'sinon';
import { stub } from 'sinon'; import { stub } from 'sinon';
import Volume from '~/src/compose/volume'; import { Volume } from '~/src/compose/volume';
import * as logTypes from '~/lib/log-types'; import * as logTypes from '~/lib/log-types';
import * as logger from '~/src/logger'; import * as logger from '~/src/logger';

View File

@ -7,7 +7,7 @@ import request from 'supertest';
import * as config from '~/src/config'; import * as config from '~/src/config';
import * as db from '~/src/db'; import * as db from '~/src/db';
import * as hostConfig from '~/src/host-config'; import * as hostConfig from '~/src/host-config';
import type Service from '~/src/compose/service'; import type { Service } from '~/src/compose/service';
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 v1 from '~/src/device-api/v1'; import * as v1 from '~/src/device-api/v1';

View File

@ -1,7 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { isRight } from 'fp-ts/lib/Either'; import { isRight } from 'fp-ts/lib/Either';
import App from '~/src/compose/app'; import { App } from '~/src/compose/app';
import Network from '~/src/compose/network'; import { Network } from '~/src/compose/network';
import * as config from '~/src/config'; import * as config from '~/src/config';
import * as testDb from '~/src/db'; import * as testDb from '~/src/db';
import * as dbFormat from '~/src/device-state/db-format'; import * as dbFormat from '~/src/device-state/db-format';

View File

@ -1,10 +1,10 @@
import App from '~/src/compose/app'; import { App } from '~/src/compose/app';
import * as imageManager from '~/src/compose/images'; import * as imageManager from '~/src/compose/images';
import type { Image } from '~/src/compose/images'; import type { Image } from '~/src/compose/images';
import Network from '~/src/compose/network'; import { Network } from '~/src/compose/network';
import Service from '~/src/compose/service'; import { Service } from '~/src/compose/service';
import type { ServiceComposeConfig } from '~/src/compose/types/service'; import type { ServiceComposeConfig } from '~/src/compose/types/service';
import type Volume from '~/src/compose/volume'; import type { Volume } from '~/src/compose/volume';
import type { import type {
CompositionStep, CompositionStep,
CompositionStepAction, CompositionStepAction,

View File

@ -1,7 +1,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import type { Image } from '~/src/compose/images'; import type { Image } from '~/src/compose/images';
import Network from '~/src/compose/network'; import { Network } from '~/src/compose/network';
import Volume from '~/src/compose/volume'; import { Volume } from '~/src/compose/volume';
import { LocksTakenMap } from '~/lib/update-lock'; import { LocksTakenMap } from '~/lib/update-lock';
import { import {

View File

@ -2,7 +2,7 @@ import { expect } from 'chai';
import type * as sinon from 'sinon'; import type * as sinon from 'sinon';
import { Network } from '~/src/compose/network'; import { Network } from '~/src/compose/network';
import type { NetworkInspectInfo } from '~/src/compose/types/network'; import type { NetworkInspectInfo } from '~/src/compose/types';
import { log } from '~/lib/supervisor-console'; import { log } from '~/lib/supervisor-console';

View File

@ -3,8 +3,8 @@ import * as _ from 'lodash';
import { expect } from 'chai'; import { expect } from 'chai';
import { createContainer } from '~/test-lib/mockerode'; import { createContainer } from '~/test-lib/mockerode';
import Service from '~/src/compose/service'; import { Service } from '~/src/compose/service';
import Volume from '~/src/compose/volume'; import { Volume } from '~/src/compose/volume';
import * as ServiceT from '~/src/compose/types/service'; import * as ServiceT from '~/src/compose/types/service';
import * as constants from '~/lib/constants'; import * as constants from '~/lib/constants';

View File

@ -1,5 +1,5 @@
import { expect } from 'chai'; import { expect } from 'chai';
import Volume from '~/src/compose/volume'; import { Volume } from '~/src/compose/volume';
describe('compose/volume: unit tests', () => { describe('compose/volume: unit tests', () => {
describe('creating a volume from a compose object', () => { describe('creating a volume from a compose object', () => {