Move composition types to compose/types

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
This commit is contained in:
Felipe Lalanne 2024-05-15 15:06:32 -04:00
parent 94de4006a0
commit 234e0de075
25 changed files with 330 additions and 281 deletions

View File

@ -6,11 +6,6 @@ import { Network } from './network';
import { Volume } from './volume';
import { Service } from './service';
import * as imageManager from './images';
import type { Image } from './images';
import type {
CompositionStep,
CompositionStepAction,
} from './composition-steps';
import { generateStep } from './composition-steps';
import type * as targetStateCache from '../device-state/target-state-cache';
import { getNetworkGateway } from '../lib/docker-utils';
@ -25,7 +20,16 @@ import { checkTruthy } from '../lib/validation';
import type { ServiceComposeConfig, DeviceMetadata } from './types/service';
import { pathExistsOnRoot } from '../lib/host-utils';
import { isSupervisor } from '../lib/supervisor-metadata';
import type { LocksTakenMap } from '../lib/update-lock';
import type {
App as AppIface,
UpdateState,
AppsToLockMap,
CompositionStep,
CompositionStepAction,
} from './types';
// Re export the type
export type App = AppIface;
export interface AppConstructOpts {
appId: number;
@ -40,43 +44,11 @@ export interface AppConstructOpts {
networks: Network[];
}
export interface UpdateState {
availableImages: Image[];
containerIds: Dictionary<string>;
downloading: string[];
locksTaken: LocksTakenMap;
force: boolean;
}
interface ChangingPair<T> {
current?: T;
target?: T;
}
export interface AppsToLockMap {
[appId: number]: Set<string>;
}
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;

View File

@ -21,26 +21,29 @@ import { getServicesLockedByAppId, LocksTakenMap } from '../lib/update-lock';
import { checkTruthy } from '../lib/validation';
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 { generateStep, getExecutors } from './composition-steps';
import type {
InstancedAppState,
TargetApps,
DeviceLegacyReport,
AppState,
ServiceState,
} from '../types/state';
import type { Image } from './images';
import type { CompositionStep, CompositionStepT } from './composition-steps';
} from '../types';
import type {
CompositionStep,
CompositionStepT,
UpdateState,
Service,
Network,
Volume,
Image,
InstancedAppState,
} from './types';
type ApplicationManagerEventEmitter = StrictEventEmitter<
EventEmitter,

View File

@ -1,104 +1,19 @@
import * as config from '../config';
import type { Image } from './images';
import type { CompositionStepArgs, Image, CompositionStep } from './types';
import * as images from './images';
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 * as commitStore from './commit';
import * as updateLock from '../lib/update-lock';
import type { DeviceLegacyReport } from '../types/state';
import type { CompositionStepAction, CompositionStepT } from './types';
interface CompositionStepArgs {
stop: {
current: Service;
options?: {
wait?: boolean;
};
};
kill: {
current: Service;
options?: {
wait?: boolean;
};
};
remove: {
current: Service;
};
updateMetadata: {
current: Service;
target: Service;
};
restart: {
current: Service;
target: Service;
};
start: {
target: Service;
};
updateCommit: {
target: string;
appId: number;
};
handover: {
current: Service;
target: Service;
options?: {
timeout?: number;
};
};
fetch: {
image: Image;
serviceName: string;
};
removeImage: {
image: Image;
};
saveImage: {
image: Image;
};
cleanup: object;
createNetwork: {
target: Network;
};
createVolume: {
target: Volume;
};
removeNetwork: {
current: Network;
};
removeVolume: {
current: Volume;
};
ensureSupervisorNetwork: object;
noop: object;
takeLock: {
appId: number;
services: string[];
force: boolean;
};
releaseLock: {
appId: number;
};
}
export type CompositionStepAction = keyof CompositionStepArgs;
export type CompositionStepT<T extends CompositionStepAction> = {
action: T;
} & CompositionStepArgs[T];
export type CompositionStep = CompositionStepT<CompositionStepAction>;
export function generateStep<T extends CompositionStepAction>(
action: T,
args: CompositionStepArgs[T],
): CompositionStep {
return {
action,
...args,
};
}
export type {
CompositionStep,
CompositionStepT,
CompositionStepAction,
} from './types';
type Executors<T extends CompositionStepAction> = {
[key in T]: (step: CompositionStepT<key>) => Promise<unknown>;
@ -114,6 +29,16 @@ interface CompositionCallbacks {
bestDeltaSource: (image: Image, available: Image[]) => string | null;
}
export function generateStep<T extends CompositionStepAction>(
action: T,
args: CompositionStepArgs[T],
): CompositionStep {
return {
action,
...args,
};
}
export function getExecutors(app: { callbacks: CompositionCallbacks }) {
const executors: Executors<CompositionStepAction> = {
stop: async (step) => {

View File

@ -24,40 +24,13 @@ import { strict as assert } from 'assert';
import log from '../lib/supervisor-console';
import { setTimeout } from 'timers/promises';
import type { Image } from './types';
export type { Image } from './types';
interface FetchProgressEvent {
percentage: number;
}
export interface Image {
id?: number;
/**
* image [registry/]repo@digest or [registry/]repo:tag
*/
name: string;
/**
* @deprecated to be removed in target state v4
*/
appId: number;
appUuid: string;
/**
* @deprecated to be removed in target state v4
*/
serviceId: number;
serviceName: string;
/**
* @deprecated to be removed in target state v4
*/
imageId: number;
/**
* @deprecated to be removed in target state v4
*/
releaseId: number;
commit: string;
dockerImageId?: string;
status?: 'Downloading' | 'Downloaded' | 'Deleting';
downloadProgress?: number | null;
}
// Setup an event emitter
interface ImageEvents {
change: void;

View File

@ -11,25 +11,13 @@ import type {
ComposeNetworkConfig,
NetworkConfig,
NetworkInspectInfo,
Network as NetworkIface,
} from './types';
import { InvalidNetworkNameError } from './errors';
import { InternalInconsistencyError } from '../lib/errors';
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;
}
export type Network = NetworkIface;
class NetworkImpl implements Network {
public appId: number;

View File

@ -20,8 +20,8 @@ import {
} from '../lib/errors';
import * as LogTypes from '../lib/log-types';
import { checkInt, isValidDeviceName } from '../lib/validation';
import type { ServiceStatus } from './service';
import { Service } from './service';
import type { ServiceStatus } from './types';
import { serviceNetworksToDockerNetworks } from './utils';
import log from '../lib/supervisor-console';

View File

@ -23,6 +23,8 @@ import type {
ConfigMap,
DeviceMetadata,
DockerDevice,
ServiceStatus,
Service as ServiceIface,
} from './types';
import {
ShortMount,
@ -41,60 +43,7 @@ const CONTAINER_NETWORK_MODE_REGEX = /container:\s*(.+)/;
const unsupportedSecurityOpt = (opt: string) => /label=.*/.test(opt);
export type ServiceStatus =
| 'Stopping'
| 'Running'
| 'Installing'
| 'Installed'
| 'Dead'
| 'paused'
| 'restarting'
| 'removing'
| 'exited';
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[];
}
export type Service = ServiceIface;
class ServiceImpl implements Service {
public appId: number;

38
src/compose/types/app.ts Normal file
View File

@ -0,0 +1,38 @@
import type { Network } from './network';
import type { Volume } from './volume';
import type { Service } from './service';
import type { LocksTakenMap } from '../../lib/update-lock';
import type { Image } from './image';
import type { CompositionStep } from './composition-step';
export interface UpdateState {
availableImages: Image[];
containerIds: Dictionary<string>;
downloading: string[];
locksTaken: LocksTakenMap;
force: boolean;
}
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[];
}
export interface AppsToLockMap {
[appId: number]: Set<string>;
}

View File

@ -0,0 +1,3 @@
import type { App } from './app';
export type InstancedAppState = { [appId: number]: App };

View File

@ -0,0 +1,83 @@
import type { Image } from './image';
import type { Service } from './service';
import type { Network } from './network';
import type { Volume } from './volume';
export interface CompositionStepArgs {
stop: {
current: Service;
options?: {
wait?: boolean;
};
};
kill: {
current: Service;
options?: {
wait?: boolean;
};
};
remove: {
current: Service;
};
updateMetadata: {
current: Service;
target: Service;
};
restart: {
current: Service;
target: Service;
};
start: {
target: Service;
};
updateCommit: {
target: string;
appId: number;
};
handover: {
current: Service;
target: Service;
options?: {
timeout?: number;
};
};
fetch: {
image: Image;
serviceName: string;
};
removeImage: {
image: Image;
};
saveImage: {
image: Image;
};
cleanup: object;
createNetwork: {
target: Network;
};
createVolume: {
target: Volume;
};
removeNetwork: {
current: Network;
};
removeVolume: {
current: Volume;
};
ensureSupervisorNetwork: object;
noop: object;
takeLock: {
appId: number;
services: string[];
force: boolean;
};
releaseLock: {
appId: number;
};
}
export type CompositionStepAction = keyof CompositionStepArgs;
export type CompositionStepT<T extends CompositionStepAction> = {
action: T;
} & CompositionStepArgs[T];
export type CompositionStep = CompositionStepT<CompositionStepAction>;

View File

@ -0,0 +1,29 @@
export interface Image {
id?: number;
/**
* image [registry/]repo@digest or [registry/]repo:tag
*/
name: string;
/**
* @deprecated to be removed in target state v4
*/
appId: number;
appUuid: string;
/**
* @deprecated to be removed in target state v4
*/
serviceId: number;
serviceName: string;
/**
* @deprecated to be removed in target state v4
*/
imageId: number;
/**
* @deprecated to be removed in target state v4
*/
releaseId: number;
commit: string;
dockerImageId?: string;
status?: 'Downloading' | 'Downloaded' | 'Deleting';
downloadProgress?: number | null;
}

View File

@ -1,2 +1,7 @@
export * from './service';
export * from './network';
export type * from './volume';
export type * from './image';
export type * from './composition-step';
export type * from './app';
export type * from './application-manager';

View File

@ -1,4 +1,7 @@
import type { NetworkInspectInfo as DockerNetworkInspectInfo } from 'dockerode';
import type {
NetworkInspectInfo as DockerNetworkInspectInfo,
NetworkCreateOptions,
} from 'dockerode';
// TODO: ConfigOnly is part of @types/dockerode@v3.2.0, but that version isn't
// compatible with `resin-docker-build` which is used for `npm run sync`.
@ -44,3 +47,18 @@ export interface NetworkConfig {
options: { [optName: string]: string };
configOnly: boolean;
}
export interface Network {
appId: number;
appUuid?: string;
name: string;
config: NetworkConfig;
isEqualConfig(network: Network): boolean;
create(): Promise<void>;
remove(): Promise<void>;
toDockerConfig(): NetworkCreateOptions & {
ConfigOnly: boolean;
};
toComposeObject(): ComposeNetworkConfig;
}

View File

@ -336,3 +336,58 @@ export interface DockerDevice {
PathInContainer: string;
CgroupPermissions: string;
}
export type ServiceStatus =
| 'Stopping'
| 'Running'
| 'Installing'
| 'Installed'
| 'Dead'
| 'paused'
| 'restarting'
| 'removing'
| 'exited';
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[];
}

View File

@ -0,0 +1,25 @@
import type { LabelObject } from '../../types';
import type { VolumeInspectInfo } from 'dockerode';
export interface VolumeConfig {
labels: LabelObject;
driver: string;
driverOpts: VolumeInspectInfo['Options'];
}
export interface ComposeVolumeConfig {
driver: string;
driver_opts: Dictionary<string>;
labels: LabelObject;
}
export interface Volume {
name: string;
appId: number;
appUuid: string;
config: VolumeConfig;
isEqualConfig(volume: Volume): boolean;
create(): Promise<void>;
remove(): Promise<void>;
}

View File

@ -2,7 +2,7 @@ import * as imageManager from './images';
import type { Service } from './service';
import type { CompositionStep } from './composition-steps';
import { generateStep } from './composition-steps';
import type { AppsToLockMap } from './app';
import type { AppsToLockMap } from './types';
import { InternalInconsistencyError } from '../lib/errors';
import { checkString } from '../lib/validation';

View File

@ -5,7 +5,6 @@ import { parse as parseCommand } from 'shell-quote';
import * as constants from '../lib/constants';
import { checkTruthy } from '../lib/validation';
import type { Service } from './service';
import type {
ComposeHealthcheck,
ConfigMap,
@ -16,6 +15,7 @@ import type {
ServiceHealthcheck,
LongDefinition,
LongBind,
Service,
} from './types';
import log from '../lib/supervisor-console';

View File

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

View File

@ -10,28 +10,13 @@ import type { LabelObject } from '../types';
import * as logger from '../logger';
import * as ComposeUtils from './utils';
export interface VolumeConfig {
labels: LabelObject;
driver: string;
driverOpts: Docker.VolumeInspectInfo['Options'];
}
import type {
Volume as VolumeIface,
VolumeConfig,
ComposeVolumeConfig,
} from './types';
export interface ComposeVolumeConfig {
driver: string;
driver_opts: Dictionary<string>;
labels: LabelObject;
}
export interface Volume {
name: string;
appId: number;
appUuid: string;
config: VolumeConfig;
isEqualConfig(volume: Volume): boolean;
create(): Promise<void>;
remove(): Promise<void>;
}
export type Volume = VolumeIface;
class VolumeImpl implements Volume {
private constructor(

View File

@ -34,7 +34,6 @@ import * as commitStore from './compose/commit';
import type {
DeviceLegacyState,
InstancedDeviceState,
DeviceState,
DeviceReport,
AppState,
@ -47,11 +46,20 @@ import type {
import * as fsUtils from './lib/fs-utils';
import { pathOnRoot } from './lib/host-utils';
import { setTimeout } from 'timers/promises';
import type { InstancedAppState } from './compose/types';
const TARGET_STATE_CONFIG_DUMP = pathOnRoot(
'/tmp/balena-supervisor/target-state-config',
);
interface InstancedDeviceState {
local: {
name: string;
config: Dictionary<string>;
apps: InstancedAppState;
};
}
function parseTargetState(state: unknown): TargetState {
const res = TargetState.decode(state);

View File

@ -8,12 +8,12 @@ import { App } from '../compose/app';
import * as images from '../compose/images';
import type {
InstancedAppState,
TargetApp,
TargetApps,
TargetRelease,
TargetService,
} from '../types/state';
import type { InstancedAppState } from '../compose/types';
type InstancedApp = InstancedAppState[0];

View File

@ -1,8 +1,5 @@
import * as t from 'io-ts';
// TODO: move all these exported types to ../compose/types
import type { ComposeNetworkConfig } from '../compose/types';
import type { ComposeVolumeConfig } from '../compose/volume';
import type { ContractObject } from '../lib/contracts';
import {
@ -16,7 +13,10 @@ import {
nonEmptyRecord,
} from './basic';
import type { App } from '../compose/app';
import type {
ComposeVolumeConfig,
ComposeNetworkConfig,
} from '../compose/types';
export type DeviceLegacyReport = Partial<{
api_port: number;
@ -356,13 +356,3 @@ export const AppsJsonFormat = t.intersection([
t.partial({ pinDevice: t.boolean }),
]);
export type AppsJsonFormat = t.TypeOf<typeof AppsJsonFormat>;
export type InstancedAppState = { [appId: number]: App };
export interface InstancedDeviceState {
local: {
name: string;
config: Dictionary<string>;
apps: InstancedAppState;
};
}

View File

@ -19,7 +19,7 @@ import {
expectSteps,
expectNoStep,
} from '~/test-lib/state-helper';
import type { InstancedAppState } from '~/src/types';
import type { InstancedAppState } from '~/src/compose/types';
// TODO: application manager inferNextSteps still queries some stuff from
// the engine instead of receiving that information as parameter. Refactoring

View File

@ -9,7 +9,7 @@ import * as fs from 'fs/promises';
import * as hostConfig from '~/src/host-config';
import * as config from '~/src/config';
import * as applicationManager from '~/src/compose/application-manager';
import type { InstancedAppState } from '~/src/types/state';
import type { InstancedAppState } from '~/src/compose/types';
import * as updateLock from '~/lib/update-lock';
import { UpdatesLockedError } from '~/lib/errors';
import * as dbus from '~/lib/dbus';

View File

@ -9,7 +9,7 @@ import type {
CompositionStep,
CompositionStepAction,
} from '~/src/compose/composition-steps';
import type { InstancedAppState } from '~/src/types/state';
import type { InstancedAppState } from '~/src/compose/types';
export const DEFAULT_NETWORK = Network.fromComposeObject(
'default',