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 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<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 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;

View File

@ -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 {

View File

@ -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';

View File

@ -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<void>;
remove(): Promise<void>;
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;

View File

@ -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<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 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:<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;

View File

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

View File

@ -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';

View File

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

View File

@ -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;

View File

@ -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<void>;
remove(): Promise<void>;
}
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;

View File

@ -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';

View File

@ -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';

View File

@ -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 {

View File

@ -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,

View File

@ -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;

View File

@ -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';

View File

@ -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', () => {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

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 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,

View File

@ -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 {

View File

@ -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';

View File

@ -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';

View File

@ -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', () => {