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;