From 94de4006a07baada9336721c4084a00f885a6572 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Wed, 15 May 2024 13:44:05 -0400 Subject: [PATCH] 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 --- src/compose/app.ts | 32 +++++++-- src/compose/application-manager.ts | 8 +-- src/compose/composition-steps.ts | 6 +- src/compose/network.ts | 21 +++++- src/compose/service.ts | 65 ++++++++++++++----- src/compose/types/index.ts | 2 + src/compose/update-strategies.ts | 2 +- src/compose/utils.ts | 2 +- src/compose/volume-manager.ts | 2 +- src/compose/volume.ts | 15 ++++- src/device-api/actions.ts | 2 +- src/device-api/v2.ts | 2 +- src/device-state/db-format.ts | 2 +- src/lib/legacy.ts | 2 +- src/types/state.ts | 4 +- .../compose/application-manager.spec.ts | 4 +- test/integration/compose/service.spec.ts | 2 +- .../compose/volume-manager.spec.ts | 2 +- test/integration/compose/volume.spec.ts | 2 +- test/integration/device-api/v1.spec.ts | 2 +- .../device-state/db-format.spec.ts | 4 +- test/lib/state-helper.ts | 8 +-- test/unit/compose/app.spec.ts | 4 +- test/unit/compose/network.spec.ts | 2 +- test/unit/compose/service.spec.ts | 4 +- test/unit/compose/volume.spec.ts | 2 +- 26 files changed, 143 insertions(+), 60 deletions(-) create mode 100644 src/compose/types/index.ts diff --git a/src/compose/app.ts b/src/compose/app.ts index e3e1dac5..8937dbf8 100644 --- a/src/compose/app.ts +++ b/src/compose/app.ts @@ -2,9 +2,9 @@ import _ from 'lodash'; import { promises as fs } from 'fs'; import type { ImageInspectInfo } from 'dockerode'; -import Network from './network'; -import Volume from './volume'; -import Service from './service'; +import { Network } from './network'; +import { Volume } from './volume'; +import { Service } from './service'; import * as imageManager from './images'; import type { Image } from './images'; import type { @@ -57,7 +57,27 @@ export interface AppsToLockMap { [appId: number]: Set; } -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 & { keepVolumes: boolean }, + ): CompositionStep[]; +} + +class AppImpl implements App { public appId: number; public appUuid?: string; // 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, appUuid: app.uuid, @@ -1044,4 +1064,4 @@ export class App { } } -export default App; +export const App = AppImpl; diff --git a/src/compose/application-manager.ts b/src/compose/application-manager.ts index 816d00b5..936b051b 100644 --- a/src/compose/application-manager.ts +++ b/src/compose/application-manager.ts @@ -20,16 +20,16 @@ import { import { getServicesLockedByAppId, LocksTakenMap } from '../lib/update-lock'; import { checkTruthy } from '../lib/validation'; -import App from './app'; +import { App } from './app'; import type { UpdateState } from './app'; import * as volumeManager from './volume-manager'; import * as networkManager from './network-manager'; import * as serviceManager from './service-manager'; import * as imageManager from './images'; import * as commitStore from './commit'; -import type Service from './service'; -import type Network from './network'; -import type Volume from './volume'; +import type { Service } from './service'; +import type { Network } from './network'; +import type { Volume } from './volume'; import { generateStep, getExecutors } from './composition-steps'; import type { diff --git a/src/compose/composition-steps.ts b/src/compose/composition-steps.ts index ded3b966..ae3c1455 100644 --- a/src/compose/composition-steps.ts +++ b/src/compose/composition-steps.ts @@ -1,12 +1,12 @@ import * as config from '../config'; import type { Image } from './images'; import * as images from './images'; -import type Network from './network'; -import type Service from './service'; +import type { Network } from './network'; +import type { Service } from './service'; import * as serviceManager from './service-manager'; import * as networkManager from './network-manager'; import * as volumeManager from './volume-manager'; -import type Volume from './volume'; +import type { Volume } from './volume'; import * as commitStore from './commit'; import * as updateLock from '../lib/update-lock'; import type { DeviceLegacyReport } from '../types/state'; diff --git a/src/compose/network.ts b/src/compose/network.ts index 7f7450d2..f55d3ae6 100644 --- a/src/compose/network.ts +++ b/src/compose/network.ts @@ -11,12 +11,27 @@ import type { ComposeNetworkConfig, NetworkConfig, NetworkInspectInfo, -} from './types/network'; +} from './types'; import { InvalidNetworkNameError } from './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; + remove(): Promise; + toDockerConfig(): dockerode.NetworkCreateOptions & { + ConfigOnly: boolean; + }; + toComposeObject(): ComposeNetworkConfig; +} + +class NetworkImpl implements Network { public appId: number; public appUuid?: string; public name: string; @@ -303,4 +318,4 @@ export class Network { } } -export default Network; +export const Network = NetworkImpl; diff --git a/src/compose/service.ts b/src/compose/service.ts index 0e73f071..64dc6814 100644 --- a/src/compose/service.ts +++ b/src/compose/service.ts @@ -23,7 +23,7 @@ import type { ConfigMap, DeviceMetadata, DockerDevice, -} from './types/service'; +} from './types'; import { ShortMount, ShortBind, @@ -34,7 +34,7 @@ import { LongBind, LongAnonymousVolume, LongNamedVolume, -} from './types/service'; +} from './types'; const SERVICE_NETWORK_MODE_REGEX = /service:\s*(.+)/; const CONTAINER_NETWORK_MODE_REGEX = /container:\s*(.+)/; @@ -52,7 +52,51 @@ export type ServiceStatus = | 'removing' | '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, + ): boolean; + isEqualConfig( + service: Service, + currentContainerIds: Dictionary, + ): boolean; + hasNetworkMode(networkName: string): boolean; + extraNetworksToJoin(): ServiceConfig['networks']; + toDockerContainer(opts: { + deviceName: string; + containerIds: Dictionary; + }): Dockerode.ContainerCreateOptions; + handoverCompleteFullPathsOnHost(): string[]; +} + +class ServiceImpl implements Service { public appId: number; public appUuid?: string; public imageId: number; @@ -64,17 +108,8 @@ export class Service { public imageName: string | null; public containerId: string | null; public exitErrorMessage: string | null; - public dependsOn: 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 createdAt: Date | null; @@ -95,7 +130,7 @@ export class Service { 'dnsSearch', ]; public static allConfigArrayFields: ServiceConfigArrayField[] = - Service.configArrayFields.concat(Service.orderedConfigArrayFields); + ServiceImpl.configArrayFields.concat(ServiceImpl.orderedConfigArrayFields); // A list of fields to ignore when comparing container configuration private static omitFields = [ @@ -109,7 +144,7 @@ export class Service { // These fields are special case, due to network_mode:service: 'networkMode', 'hostname', - ].concat(Service.allConfigArrayFields); + ].concat(ServiceImpl.allConfigArrayFields); private constructor() { /* do not allow instancing a service object with `new` */ @@ -1170,4 +1205,4 @@ export class Service { } } -export default Service; +export const Service = ServiceImpl; diff --git a/src/compose/types/index.ts b/src/compose/types/index.ts new file mode 100644 index 00000000..1c3ae751 --- /dev/null +++ b/src/compose/types/index.ts @@ -0,0 +1,2 @@ +export * from './service'; +export * from './network'; diff --git a/src/compose/update-strategies.ts b/src/compose/update-strategies.ts index 8d508572..bdd318ce 100644 --- a/src/compose/update-strategies.ts +++ b/src/compose/update-strategies.ts @@ -1,5 +1,5 @@ import * as imageManager from './images'; -import type Service from './service'; +import type { Service } from './service'; import type { CompositionStep } from './composition-steps'; import { generateStep } from './composition-steps'; import type { AppsToLockMap } from './app'; diff --git a/src/compose/utils.ts b/src/compose/utils.ts index 0a57a4e2..6c8b7750 100644 --- a/src/compose/utils.ts +++ b/src/compose/utils.ts @@ -16,7 +16,7 @@ import type { ServiceHealthcheck, LongDefinition, LongBind, -} from './types/service'; +} from './types'; import log from '../lib/supervisor-console'; diff --git a/src/compose/volume-manager.ts b/src/compose/volume-manager.ts index 52590844..ed29f348 100644 --- a/src/compose/volume-manager.ts +++ b/src/compose/volume-manager.ts @@ -11,7 +11,7 @@ import log from '../lib/supervisor-console'; import * as logger from '../logger'; import { ResourceRecreationAttemptError } from './errors'; import type { VolumeConfig } from './volume'; -import Volume from './volume'; +import { Volume } from './volume'; export interface VolumeNameOpts { name: string; diff --git a/src/compose/volume.ts b/src/compose/volume.ts index da5f6a72..d87d8e23 100644 --- a/src/compose/volume.ts +++ b/src/compose/volume.ts @@ -22,7 +22,18 @@ export interface ComposeVolumeConfig { labels: LabelObject; } -export class Volume { +export interface Volume { + name: string; + appId: number; + appUuid: string; + config: VolumeConfig; + + isEqualConfig(volume: Volume): boolean; + create(): Promise; + remove(): Promise; +} + +class VolumeImpl implements Volume { private constructor( public name: string, public appId: number, @@ -162,4 +173,4 @@ export class Volume { } } -export default Volume; +export const Volume = VolumeImpl; diff --git a/src/device-api/actions.ts b/src/device-api/actions.ts index 982f6f54..3fee8be2 100644 --- a/src/device-api/actions.ts +++ b/src/device-api/actions.ts @@ -11,7 +11,7 @@ import * as applicationManager from '../compose/application-manager'; import type { CompositionStepAction } from '../compose/composition-steps'; import { generateStep } from '../compose/composition-steps'; 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 * as TargetState from '../device-state/target-state'; import log from '../lib/supervisor-console'; diff --git a/src/device-api/v2.ts b/src/device-api/v2.ts index 874f3825..f973549c 100644 --- a/src/device-api/v2.ts +++ b/src/device-api/v2.ts @@ -7,7 +7,7 @@ import * as apiBinder from '../api-binder'; import * as applicationManager from '../compose/application-manager'; import type { CompositionStepAction } from '../compose/composition-steps'; import type { Service } from '../compose/service'; -import Volume from '../compose/volume'; +import { Volume } from '../compose/volume'; import * as commitStore from '../compose/commit'; import * as config from '../config'; import * as db from '../db'; diff --git a/src/device-state/db-format.ts b/src/device-state/db-format.ts index 9c687491..ebe8c2f7 100644 --- a/src/device-state/db-format.ts +++ b/src/device-state/db-format.ts @@ -4,7 +4,7 @@ import type * as db from '../db'; import * as targetStateCache 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 type { diff --git a/src/lib/legacy.ts b/src/lib/legacy.ts index a97c8b75..12d20207 100644 --- a/src/lib/legacy.ts +++ b/src/lib/legacy.ts @@ -14,7 +14,7 @@ import { import { docker } from './docker-utils'; import { log } from './supervisor-console'; import { pathOnData } from './host-utils'; -import type Volume from '../compose/volume'; +import type { Volume } from '../compose/volume'; import * as logger from '../logger'; import type { DatabaseApp, diff --git a/src/types/state.ts b/src/types/state.ts index 6d9663e0..46f6719f 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -1,7 +1,7 @@ import * as t from 'io-ts'; // 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 { ContractObject } from '../lib/contracts'; @@ -16,7 +16,7 @@ import { nonEmptyRecord, } from './basic'; -import type App from '../compose/app'; +import type { App } from '../compose/app'; export type DeviceLegacyReport = Partial<{ api_port: number; diff --git a/test/integration/compose/application-manager.spec.ts b/test/integration/compose/application-manager.spec.ts index f5f204be..baacc711 100644 --- a/test/integration/compose/application-manager.spec.ts +++ b/test/integration/compose/application-manager.spec.ts @@ -4,9 +4,9 @@ import Docker from 'dockerode'; import * as applicationManager from '~/src/compose/application-manager'; import * as imageManager from '~/src/compose/images'; 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 Volume from '~/src/compose/volume'; +import { Volume } from '~/src/compose/volume'; import * as config from '~/src/config'; import { LocksTakenMap } from '~/lib/update-lock'; import { createDockerImage } from '~/test-lib/docker-helper'; diff --git a/test/integration/compose/service.spec.ts b/test/integration/compose/service.spec.ts index f4ae94df..fb0e9ed2 100644 --- a/test/integration/compose/service.spec.ts +++ b/test/integration/compose/service.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import Service from '~/src/compose/service'; +import { Service } from '~/src/compose/service'; import * as deviceApi from '~/src/device-api'; describe('compose/service: integration tests', () => { diff --git a/test/integration/compose/volume-manager.spec.ts b/test/integration/compose/volume-manager.spec.ts index 395a7ef9..a9039326 100644 --- a/test/integration/compose/volume-manager.spec.ts +++ b/test/integration/compose/volume-manager.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; 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 Docker from 'dockerode'; diff --git a/test/integration/compose/volume.spec.ts b/test/integration/compose/volume.spec.ts index b84d4f9c..7268d3f4 100644 --- a/test/integration/compose/volume.spec.ts +++ b/test/integration/compose/volume.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import type { SinonStub } 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 logger from '~/src/logger'; diff --git a/test/integration/device-api/v1.spec.ts b/test/integration/device-api/v1.spec.ts index bc621347..bd1e9b0b 100644 --- a/test/integration/device-api/v1.spec.ts +++ b/test/integration/device-api/v1.spec.ts @@ -7,7 +7,7 @@ import request from 'supertest'; import * as config from '~/src/config'; import * as db from '~/src/db'; 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 actions from '~/src/device-api/actions'; import * as v1 from '~/src/device-api/v1'; diff --git a/test/integration/device-state/db-format.spec.ts b/test/integration/device-state/db-format.spec.ts index 9f99bc50..fcd1e985 100644 --- a/test/integration/device-state/db-format.spec.ts +++ b/test/integration/device-state/db-format.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { isRight } from 'fp-ts/lib/Either'; -import App from '~/src/compose/app'; -import Network from '~/src/compose/network'; +import { App } from '~/src/compose/app'; +import { Network } from '~/src/compose/network'; import * as config from '~/src/config'; import * as testDb from '~/src/db'; import * as dbFormat from '~/src/device-state/db-format'; diff --git a/test/lib/state-helper.ts b/test/lib/state-helper.ts index b2f5bea3..7a54518c 100644 --- a/test/lib/state-helper.ts +++ b/test/lib/state-helper.ts @@ -1,10 +1,10 @@ -import App from '~/src/compose/app'; +import { App } from '~/src/compose/app'; import * as imageManager from '~/src/compose/images'; import type { Image } from '~/src/compose/images'; -import Network from '~/src/compose/network'; -import Service from '~/src/compose/service'; +import { Network } from '~/src/compose/network'; +import { Service } from '~/src/compose/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 { CompositionStep, CompositionStepAction, diff --git a/test/unit/compose/app.spec.ts b/test/unit/compose/app.spec.ts index e1a9671e..fdb5e2c6 100644 --- a/test/unit/compose/app.spec.ts +++ b/test/unit/compose/app.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import type { Image } from '~/src/compose/images'; -import Network from '~/src/compose/network'; -import Volume from '~/src/compose/volume'; +import { Network } from '~/src/compose/network'; +import { Volume } from '~/src/compose/volume'; import { LocksTakenMap } from '~/lib/update-lock'; import { diff --git a/test/unit/compose/network.spec.ts b/test/unit/compose/network.spec.ts index 0bdea390..1d945f8c 100644 --- a/test/unit/compose/network.spec.ts +++ b/test/unit/compose/network.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import type * as sinon from 'sinon'; 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'; diff --git a/test/unit/compose/service.spec.ts b/test/unit/compose/service.spec.ts index a55e8ee7..1c7d262e 100644 --- a/test/unit/compose/service.spec.ts +++ b/test/unit/compose/service.spec.ts @@ -3,8 +3,8 @@ import * as _ from 'lodash'; import { expect } from 'chai'; import { createContainer } from '~/test-lib/mockerode'; -import Service from '~/src/compose/service'; -import Volume from '~/src/compose/volume'; +import { Service } from '~/src/compose/service'; +import { Volume } from '~/src/compose/volume'; import * as ServiceT from '~/src/compose/types/service'; import * as constants from '~/lib/constants'; diff --git a/test/unit/compose/volume.spec.ts b/test/unit/compose/volume.spec.ts index a636112b..94a5ab4d 100644 --- a/test/unit/compose/volume.spec.ts +++ b/test/unit/compose/volume.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import Volume from '~/src/compose/volume'; +import { Volume } from '~/src/compose/volume'; describe('compose/volume: unit tests', () => { describe('creating a volume from a compose object', () => {