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 1/7] 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', () => { From 234e0de07599c1033d04c77d2df9a000d38f60dd Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Wed, 15 May 2024 15:06:32 -0400 Subject: [PATCH 2/7] 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 --- src/compose/app.ts | 48 ++------ src/compose/application-manager.ts | 19 +-- src/compose/composition-steps.ts | 109 +++--------------- src/compose/images.ts | 33 +----- src/compose/network.ts | 16 +-- src/compose/service-manager.ts | 2 +- src/compose/service.ts | 57 +-------- src/compose/types/app.ts | 38 ++++++ src/compose/types/application-manager.ts | 3 + src/compose/types/composition-step.ts | 83 +++++++++++++ src/compose/types/image.ts | 29 +++++ src/compose/types/index.ts | 5 + src/compose/types/network.ts | 20 +++- src/compose/types/service.ts | 55 +++++++++ src/compose/types/volume.ts | 25 ++++ src/compose/update-strategies.ts | 2 +- src/compose/utils.ts | 2 +- src/compose/volume-manager.ts | 2 +- src/compose/volume.ts | 27 +---- src/device-state.ts | 10 +- src/device-state/db-format.ts | 2 +- src/types/state.ts | 18 +-- .../compose/application-manager.spec.ts | 2 +- test/integration/host-config.spec.ts | 2 +- test/lib/state-helper.ts | 2 +- 25 files changed, 330 insertions(+), 281 deletions(-) create mode 100644 src/compose/types/app.ts create mode 100644 src/compose/types/application-manager.ts create mode 100644 src/compose/types/composition-step.ts create mode 100644 src/compose/types/image.ts create mode 100644 src/compose/types/volume.ts diff --git a/src/compose/app.ts b/src/compose/app.ts index 8937dbf8..d04c21e0 100644 --- a/src/compose/app.ts +++ b/src/compose/app.ts @@ -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; - downloading: string[]; - locksTaken: LocksTakenMap; - force: boolean; -} - interface ChangingPair { current?: T; target?: T; } -export interface AppsToLockMap { - [appId: number]: Set; -} - -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; diff --git a/src/compose/application-manager.ts b/src/compose/application-manager.ts index 936b051b..f19c39b3 100644 --- a/src/compose/application-manager.ts +++ b/src/compose/application-manager.ts @@ -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, diff --git a/src/compose/composition-steps.ts b/src/compose/composition-steps.ts index ae3c1455..c2d4fb08 100644 --- a/src/compose/composition-steps.ts +++ b/src/compose/composition-steps.ts @@ -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 = { - action: T; -} & CompositionStepArgs[T]; -export type CompositionStep = CompositionStepT; - -export function generateStep( - action: T, - args: CompositionStepArgs[T], -): CompositionStep { - return { - action, - ...args, - }; -} +export type { + CompositionStep, + CompositionStepT, + CompositionStepAction, +} from './types'; type Executors = { [key in T]: (step: CompositionStepT) => Promise; @@ -114,6 +29,16 @@ interface CompositionCallbacks { bestDeltaSource: (image: Image, available: Image[]) => string | null; } +export function generateStep( + action: T, + args: CompositionStepArgs[T], +): CompositionStep { + return { + action, + ...args, + }; +} + export function getExecutors(app: { callbacks: CompositionCallbacks }) { const executors: Executors = { stop: async (step) => { diff --git a/src/compose/images.ts b/src/compose/images.ts index 13c52457..687a5bec 100644 --- a/src/compose/images.ts +++ b/src/compose/images.ts @@ -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; diff --git a/src/compose/network.ts b/src/compose/network.ts index f55d3ae6..50d0353a 100644 --- a/src/compose/network.ts +++ b/src/compose/network.ts @@ -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; - remove(): Promise; - toDockerConfig(): dockerode.NetworkCreateOptions & { - ConfigOnly: boolean; - }; - toComposeObject(): ComposeNetworkConfig; -} +export type Network = NetworkIface; class NetworkImpl implements Network { public appId: number; diff --git a/src/compose/service-manager.ts b/src/compose/service-manager.ts index cb782ecf..f4e55f3b 100644 --- a/src/compose/service-manager.ts +++ b/src/compose/service-manager.ts @@ -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'; diff --git a/src/compose/service.ts b/src/compose/service.ts index 64dc6814..7bd454d4 100644 --- a/src/compose/service.ts +++ b/src/compose/service.ts @@ -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, - ): boolean; - isEqualConfig( - service: Service, - currentContainerIds: Dictionary, - ): boolean; - hasNetworkMode(networkName: string): boolean; - extraNetworksToJoin(): ServiceConfig['networks']; - toDockerContainer(opts: { - deviceName: string; - containerIds: Dictionary; - }): Dockerode.ContainerCreateOptions; - handoverCompleteFullPathsOnHost(): string[]; -} +export type Service = ServiceIface; class ServiceImpl implements Service { public appId: number; diff --git a/src/compose/types/app.ts b/src/compose/types/app.ts new file mode 100644 index 00000000..3b700e9b --- /dev/null +++ b/src/compose/types/app.ts @@ -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; + 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 & { keepVolumes: boolean }, + ): CompositionStep[]; +} + +export interface AppsToLockMap { + [appId: number]: Set; +} diff --git a/src/compose/types/application-manager.ts b/src/compose/types/application-manager.ts new file mode 100644 index 00000000..5afdd7af --- /dev/null +++ b/src/compose/types/application-manager.ts @@ -0,0 +1,3 @@ +import type { App } from './app'; + +export type InstancedAppState = { [appId: number]: App }; diff --git a/src/compose/types/composition-step.ts b/src/compose/types/composition-step.ts new file mode 100644 index 00000000..ddbe0460 --- /dev/null +++ b/src/compose/types/composition-step.ts @@ -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 = { + action: T; +} & CompositionStepArgs[T]; +export type CompositionStep = CompositionStepT; diff --git a/src/compose/types/image.ts b/src/compose/types/image.ts new file mode 100644 index 00000000..26e8bc5b --- /dev/null +++ b/src/compose/types/image.ts @@ -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; +} diff --git a/src/compose/types/index.ts b/src/compose/types/index.ts index 1c3ae751..08f16d60 100644 --- a/src/compose/types/index.ts +++ b/src/compose/types/index.ts @@ -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'; diff --git a/src/compose/types/network.ts b/src/compose/types/network.ts index bac695d7..9fcc8790 100644 --- a/src/compose/types/network.ts +++ b/src/compose/types/network.ts @@ -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; + remove(): Promise; + toDockerConfig(): NetworkCreateOptions & { + ConfigOnly: boolean; + }; + toComposeObject(): ComposeNetworkConfig; +} diff --git a/src/compose/types/service.ts b/src/compose/types/service.ts index 35367290..7dd11fa3 100644 --- a/src/compose/types/service.ts +++ b/src/compose/types/service.ts @@ -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, + ): boolean; + isEqualConfig( + service: Service, + currentContainerIds: Dictionary, + ): boolean; + hasNetworkMode(networkName: string): boolean; + extraNetworksToJoin(): ServiceConfig['networks']; + toDockerContainer(opts: { + deviceName: string; + containerIds: Dictionary; + }): Dockerode.ContainerCreateOptions; + handoverCompleteFullPathsOnHost(): string[]; +} diff --git a/src/compose/types/volume.ts b/src/compose/types/volume.ts new file mode 100644 index 00000000..3c685cfe --- /dev/null +++ b/src/compose/types/volume.ts @@ -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; + labels: LabelObject; +} + +export interface Volume { + name: string; + appId: number; + appUuid: string; + config: VolumeConfig; + + isEqualConfig(volume: Volume): boolean; + create(): Promise; + remove(): Promise; +} diff --git a/src/compose/update-strategies.ts b/src/compose/update-strategies.ts index bdd318ce..7ba22ba7 100644 --- a/src/compose/update-strategies.ts +++ b/src/compose/update-strategies.ts @@ -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'; diff --git a/src/compose/utils.ts b/src/compose/utils.ts index 6c8b7750..839a2831 100644 --- a/src/compose/utils.ts +++ b/src/compose/utils.ts @@ -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'; diff --git a/src/compose/volume-manager.ts b/src/compose/volume-manager.ts index ed29f348..511676de 100644 --- a/src/compose/volume-manager.ts +++ b/src/compose/volume-manager.ts @@ -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 { diff --git a/src/compose/volume.ts b/src/compose/volume.ts index d87d8e23..373913ef 100644 --- a/src/compose/volume.ts +++ b/src/compose/volume.ts @@ -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; - labels: LabelObject; -} - -export interface Volume { - name: string; - appId: number; - appUuid: string; - config: VolumeConfig; - - isEqualConfig(volume: Volume): boolean; - create(): Promise; - remove(): Promise; -} +export type Volume = VolumeIface; class VolumeImpl implements Volume { private constructor( diff --git a/src/device-state.ts b/src/device-state.ts index 00e1d9a5..1956fc12 100644 --- a/src/device-state.ts +++ b/src/device-state.ts @@ -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; + apps: InstancedAppState; + }; +} + function parseTargetState(state: unknown): TargetState { const res = TargetState.decode(state); diff --git a/src/device-state/db-format.ts b/src/device-state/db-format.ts index ebe8c2f7..902d07c9 100644 --- a/src/device-state/db-format.ts +++ b/src/device-state/db-format.ts @@ -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]; diff --git a/src/types/state.ts b/src/types/state.ts index 46f6719f..542af0bb 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -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; - -export type InstancedAppState = { [appId: number]: App }; - -export interface InstancedDeviceState { - local: { - name: string; - config: Dictionary; - apps: InstancedAppState; - }; -} diff --git a/test/integration/compose/application-manager.spec.ts b/test/integration/compose/application-manager.spec.ts index baacc711..b018ee5b 100644 --- a/test/integration/compose/application-manager.spec.ts +++ b/test/integration/compose/application-manager.spec.ts @@ -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 diff --git a/test/integration/host-config.spec.ts b/test/integration/host-config.spec.ts index bbb304c5..fc296f16 100644 --- a/test/integration/host-config.spec.ts +++ b/test/integration/host-config.spec.ts @@ -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'; diff --git a/test/lib/state-helper.ts b/test/lib/state-helper.ts index 7a54518c..e33bd11e 100644 --- a/test/lib/state-helper.ts +++ b/test/lib/state-helper.ts @@ -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', From bef5e784400d13d2788748e059e36c761d956f23 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Wed, 15 May 2024 15:26:00 -0400 Subject: [PATCH 3/7] Move Compose(Network|Volume)Config to top level types This reduces dependencies from 80 to 47 Change-type: patch --- src/compose/types/network.ts | 22 ++-------------------- src/compose/types/volume.ts | 7 +------ src/types/state.ts | 30 ++++++++++++++++++++++++++---- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/compose/types/network.ts b/src/compose/types/network.ts index 9fcc8790..e89eccba 100644 --- a/src/compose/types/network.ts +++ b/src/compose/types/network.ts @@ -8,27 +8,9 @@ import type { export interface NetworkInspectInfo extends DockerNetworkInspectInfo { ConfigOnly: boolean; } +import type { ComposeNetworkConfig } from '../../types'; +export type { ComposeNetworkConfig } from '../../types'; -export interface ComposeNetworkConfig { - driver: string; - driver_opts: Dictionary; - ipam: { - driver: string; - config: Array< - Partial<{ - subnet: string; - ip_range: string; - gateway: string; - aux_addresses: Dictionary; - }> - >; - options: Dictionary; - }; - enable_ipv6: boolean; - internal: boolean; - labels: Dictionary; - config_only: boolean; -} export interface NetworkConfig { driver: string; ipam: { diff --git a/src/compose/types/volume.ts b/src/compose/types/volume.ts index 3c685cfe..b9e6fea1 100644 --- a/src/compose/types/volume.ts +++ b/src/compose/types/volume.ts @@ -1,5 +1,6 @@ import type { LabelObject } from '../../types'; import type { VolumeInspectInfo } from 'dockerode'; +export type { ComposeVolumeConfig } from '../../types'; export interface VolumeConfig { labels: LabelObject; @@ -7,12 +8,6 @@ export interface VolumeConfig { driverOpts: VolumeInspectInfo['Options']; } -export interface ComposeVolumeConfig { - driver: string; - driver_opts: Dictionary; - labels: LabelObject; -} - export interface Volume { name: string; appId: number; diff --git a/src/types/state.ts b/src/types/state.ts index 542af0bb..806c0b0d 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -13,10 +13,32 @@ import { nonEmptyRecord, } from './basic'; -import type { - ComposeVolumeConfig, - ComposeNetworkConfig, -} from '../compose/types'; +export interface ComposeVolumeConfig { + driver: string; + driver_opts: Dictionary; + labels: LabelObject; +} + +export interface ComposeNetworkConfig { + driver: string; + driver_opts: Dictionary; + ipam: { + driver: string; + config: Array< + Partial<{ + subnet: string; + ip_range: string; + gateway: string; + aux_addresses: Dictionary; + }> + >; + options: Dictionary; + }; + enable_ipv6: boolean; + internal: boolean; + labels: Dictionary; + config_only: boolean; +} export type DeviceLegacyReport = Partial<{ api_port: number; From 48f75e61b8c53c532c27eb0c3f0d878312b4f2e7 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Wed, 15 May 2024 15:52:25 -0400 Subject: [PATCH 4/7] Do not re-export ContractObject on lib/contracts Further reduce circular dependencies to 10 Change-type: patch --- src/lib/contracts.ts | 5 ++--- src/types/state.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts index 52b7f981..a63d32c7 100644 --- a/src/lib/contracts.ts +++ b/src/lib/contracts.ts @@ -3,14 +3,13 @@ import * as t from 'io-ts'; import Reporter from 'io-ts-reporters'; import _ from 'lodash'; -import { Blueprint, Contract, ContractObject } from '@balena/contrato'; +import type { ContractObject } from '@balena/contrato'; +import { Blueprint, Contract } from '@balena/contrato'; import { ContractValidationError, InternalInconsistencyError } from './errors'; import { checkTruthy } from './validation'; import type { TargetApps } from '../types'; -export { ContractObject }; - export interface ApplicationContractResult { valid: boolean; unmetServices: string[]; diff --git a/src/types/state.ts b/src/types/state.ts index 806c0b0d..cf3239dd 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -1,6 +1,6 @@ import * as t from 'io-ts'; -import type { ContractObject } from '../lib/contracts'; +import type { ContractObject } from '@balena/contrato'; import { DockerName, From ac2db38742962a31af63549f0d9f257e6a1331f3 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Wed, 15 May 2024 15:39:22 -0400 Subject: [PATCH 5/7] Move api-keys module to src/lib This removes circular dependencies between the device-api module and the compose module, reducing total circular dependencies to 15 Change-type: patch --- src/compose/utils.ts | 4 +- src/device-api/actions.ts | 2 +- src/device-api/index.ts | 6 +- src/device-api/middleware/auth.ts | 2 +- src/device-api/v1.ts | 2 +- src/device-api/v2.ts | 2 +- src/device-state.ts | 2 +- src/{device-api => lib}/api-keys.ts | 0 test/integration/compose/service.spec.ts | 4 +- test/integration/device-api/actions.spec.ts | 10 +- test/integration/device-api/index.spec.ts | 3 +- .../integration/device-api/middleware.spec.ts | 16 +- test/integration/device-api/v1.spec.ts | 137 +++++++++--------- test/integration/device-api/v2.spec.ts | 105 +++++++------- test/legacy/42-device-api-v2.spec.ts | 20 +-- .../device-api => lib}/api-keys.spec.ts | 34 ++--- 16 files changed, 171 insertions(+), 178 deletions(-) rename src/{device-api => lib}/api-keys.ts (100%) rename test/{integration/device-api => lib}/api-keys.spec.ts (72%) diff --git a/src/compose/utils.ts b/src/compose/utils.ts index 839a2831..9439cf87 100644 --- a/src/compose/utils.ts +++ b/src/compose/utils.ts @@ -20,7 +20,7 @@ import type { import log from '../lib/supervisor-console'; -import * as deviceApi from '../device-api'; +import * as apiKeys from '../lib/api-keys'; export function camelCaseConfig( literalConfig: ConfigMap, @@ -369,7 +369,7 @@ export async function addFeaturesFromLabels( }, 'io.balena.features.supervisor-api': async () => { // create a app/service specific API secret - const apiSecret = await deviceApi.generateScopedKey( + const apiSecret = await apiKeys.generateScopedKey( service.appId, service.serviceName, ); diff --git a/src/device-api/actions.ts b/src/device-api/actions.ts index 3fee8be2..74cbbfb3 100644 --- a/src/device-api/actions.ts +++ b/src/device-api/actions.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import { getGlobalApiKey, refreshKey } from '.'; +import { getGlobalApiKey, refreshKey } from '../lib/api-keys'; import * as messages from './messages'; import * as eventTracker from '../event-tracker'; import * as deviceState from '../device-state'; diff --git a/src/device-api/index.ts b/src/device-api/index.ts index 51e51295..2c99d945 100644 --- a/src/device-api/index.ts +++ b/src/device-api/index.ts @@ -1,7 +1,7 @@ import express from 'express'; import * as middleware from './middleware'; -import * as apiKeys from './api-keys'; +import type * as apiKeys from '../lib/api-keys'; import * as actions from './actions'; import log from '../lib/supervisor-console'; @@ -15,10 +15,6 @@ interface SupervisorAPIConstructOpts { // API key methods // For better black boxing, device-api should serve as the interface // to the rest of the Supervisor code for accessing API key related methods. -export const getGlobalApiKey = apiKeys.getGlobalApiKey; -export const refreshKey = apiKeys.refreshKey; -export const generateScopedKey = apiKeys.generateScopedKey; -export const getScopesForKey = apiKeys.getScopesForKey; export class SupervisorAPI { private routers: express.Router[]; diff --git a/src/device-api/middleware/auth.ts b/src/device-api/middleware/auth.ts index 2d660eff..fb343417 100644 --- a/src/device-api/middleware/auth.ts +++ b/src/device-api/middleware/auth.ts @@ -1,4 +1,4 @@ -import * as apiKeys from '../api-keys'; +import * as apiKeys from '../../lib/api-keys'; import * as config from '../../config'; import type { Request } from 'express'; diff --git a/src/device-api/v1.ts b/src/device-api/v1.ts index 10ab2cb5..27ff18e6 100644 --- a/src/device-api/v1.ts +++ b/src/device-api/v1.ts @@ -2,7 +2,7 @@ import express from 'express'; import type { Response } from 'express'; import * as actions from './actions'; -import type { AuthorizedRequest } from './api-keys'; +import type { AuthorizedRequest } from '../lib/api-keys'; import * as eventTracker from '../event-tracker'; import type * as deviceState from '../device-state'; diff --git a/src/device-api/v2.ts b/src/device-api/v2.ts index f973549c..c255b9fe 100644 --- a/src/device-api/v2.ts +++ b/src/device-api/v2.ts @@ -26,7 +26,7 @@ import { BadRequestError, } from '../lib/errors'; import { isVPNActive } from '../network'; -import type { AuthorizedRequest } from './api-keys'; +import type { AuthorizedRequest } from '../lib/api-keys'; import { fromV2TargetState } from '../lib/legacy'; import * as actions from './actions'; import { v2ServiceEndpointError } from './messages'; diff --git a/src/device-state.ts b/src/device-state.ts index 1956fc12..9e01775a 100644 --- a/src/device-state.ts +++ b/src/device-state.ts @@ -25,7 +25,7 @@ import { import * as updateLock from './lib/update-lock'; import { takeGlobalLockRO, takeGlobalLockRW } from './lib/process-lock'; import * as dbFormat from './device-state/db-format'; -import { getGlobalApiKey } from './device-api'; +import { getGlobalApiKey } from './lib/api-keys'; import * as sysInfo from './lib/system-info'; import { log } from './lib/supervisor-console'; import { loadTargetFromFile } from './device-state/preload'; diff --git a/src/device-api/api-keys.ts b/src/lib/api-keys.ts similarity index 100% rename from src/device-api/api-keys.ts rename to src/lib/api-keys.ts diff --git a/test/integration/compose/service.spec.ts b/test/integration/compose/service.spec.ts index fb0e9ed2..39bd9889 100644 --- a/test/integration/compose/service.spec.ts +++ b/test/integration/compose/service.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { Service } from '~/src/compose/service'; -import * as deviceApi from '~/src/device-api'; +import * as apiKeys from '~/lib/api-keys'; describe('compose/service: integration tests', () => { describe('Feature labels', () => { @@ -41,7 +41,7 @@ describe('compose/service: integration tests', () => { }); it('sets BALENA_API_KEY env var to the scoped API key value', async () => { - const mykey = await deviceApi.generateScopedKey(123456, 'foobar'); + const mykey = await apiKeys.generateScopedKey(123456, 'foobar'); const service = await Service.fromComposeObject( { diff --git a/test/integration/device-api/actions.spec.ts b/test/integration/device-api/actions.spec.ts index 933563cf..30a03256 100644 --- a/test/integration/device-api/actions.spec.ts +++ b/test/integration/device-api/actions.spec.ts @@ -9,7 +9,7 @@ import { testfs } from 'mocha-pod'; import * as deviceState from '~/src/device-state'; import * as config from '~/src/config'; import * as hostConfig from '~/src/host-config'; -import * as deviceApi from '~/src/device-api'; +import * as apiKeys from '~/lib/api-keys'; import * as actions from '~/src/device-api/actions'; import * as TargetState from '~/src/device-state/target-state'; import * as updateLock from '~/lib/update-lock'; @@ -56,10 +56,10 @@ describe('regenerates API keys', () => { afterEach(() => (deviceState.reportCurrentState as SinonStub).restore()); it("communicates new key to cloud if it's a global key", async () => { - const originalGlobalKey = await deviceApi.getGlobalApiKey(); + const originalGlobalKey = await apiKeys.getGlobalApiKey(); const newKey = await actions.regenerateKey(originalGlobalKey); expect(originalGlobalKey).to.not.equal(newKey); - expect(newKey).to.equal(await deviceApi.getGlobalApiKey()); + expect(newKey).to.equal(await apiKeys.getGlobalApiKey()); expect(deviceState.reportCurrentState as SinonStub).to.have.been.calledOnce; expect( (deviceState.reportCurrentState as SinonStub).firstCall.args[0], @@ -69,10 +69,10 @@ describe('regenerates API keys', () => { }); it("doesn't communicate new key if it's a service key", async () => { - const originalScopedKey = await deviceApi.generateScopedKey(111, 'main'); + const originalScopedKey = await apiKeys.generateScopedKey(111, 'main'); const newKey = await actions.regenerateKey(originalScopedKey); expect(originalScopedKey).to.not.equal(newKey); - expect(newKey).to.not.equal(await deviceApi.getGlobalApiKey()); + expect(newKey).to.not.equal(await apiKeys.getGlobalApiKey()); expect(deviceState.reportCurrentState as SinonStub).to.not.have.been.called; }); }); diff --git a/test/integration/device-api/index.spec.ts b/test/integration/device-api/index.spec.ts index 9c75903c..d2532505 100644 --- a/test/integration/device-api/index.spec.ts +++ b/test/integration/device-api/index.spec.ts @@ -1,6 +1,7 @@ import type * as express from 'express'; import request from 'supertest'; +import * as apiKeys from '~/lib/api-keys'; import * as deviceApi from '~/src/device-api'; describe('device-api/index', () => { @@ -22,7 +23,7 @@ describe('device-api/index', () => { await request(api) .get('/ping') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); }); diff --git a/test/integration/device-api/middleware.spec.ts b/test/integration/device-api/middleware.spec.ts index 2afc2fe8..58f65433 100644 --- a/test/integration/device-api/middleware.spec.ts +++ b/test/integration/device-api/middleware.spec.ts @@ -3,7 +3,7 @@ import request from 'supertest'; import * as config from '~/src/config'; import * as testDb from '~/src/db'; -import * as deviceApi from '~/src/device-api'; +import * as apiKeys from '~/lib/api-keys'; import * as middleware from '~/src/device-api/middleware'; describe('device-api/middleware', () => { @@ -35,7 +35,7 @@ describe('device-api/middleware', () => { it('validates API key from request query', async () => { await request(app) - .get(`/?apikey=${await deviceApi.getGlobalApiKey()}`) + .get(`/?apikey=${await apiKeys.getGlobalApiKey()}`) .expect(200); await request(app).get(`/?apikey=${INVALID_KEY}`).expect(401); @@ -44,7 +44,7 @@ describe('device-api/middleware', () => { const cases = ['ApiKey', 'apiKey', 'APIKEY', 'ApIKeY']; for (const query of cases) { await request(app) - .get(`/?${query}=${await deviceApi.getGlobalApiKey()}`) + .get(`/?${query}=${await apiKeys.getGlobalApiKey()}`) .expect(401); } }); @@ -55,10 +55,7 @@ describe('device-api/middleware', () => { for (const scheme of cases) { await request(app) .get('/') - .set( - 'Authorization', - `${scheme} ${await deviceApi.getGlobalApiKey()}`, - ) + .set('Authorization', `${scheme} ${await apiKeys.getGlobalApiKey()}`) .expect(200); await request(app) @@ -74,10 +71,7 @@ describe('device-api/middleware', () => { for (const scheme of cases) { await request(app) .get('/') - .set( - 'Authorization', - `${scheme} ${await deviceApi.getGlobalApiKey()}`, - ) + .set('Authorization', `${scheme} ${await apiKeys.getGlobalApiKey()}`) .expect(200); await request(app) diff --git a/test/integration/device-api/v1.spec.ts b/test/integration/device-api/v1.spec.ts index bd1e9b0b..fdcfd60d 100644 --- a/test/integration/device-api/v1.spec.ts +++ b/test/integration/device-api/v1.spec.ts @@ -8,6 +8,7 @@ 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 * as apiKeys from '~/lib/api-keys'; import * as deviceApi from '~/src/device-api'; import * as actions from '~/src/device-api/actions'; import * as v1 from '~/src/device-api/v1'; @@ -73,7 +74,7 @@ describe('device-api/v1', () => { it('responds with 200', async () => { await request(api) .post('/v1/blink') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); }); @@ -84,7 +85,7 @@ describe('device-api/v1', () => { afterEach(() => (actions.regenerateKey as SinonStub).restore()); it('responds with 200 and valid new API key', async () => { - const oldKey = await deviceApi.getGlobalApiKey(); + const oldKey = await apiKeys.getGlobalApiKey(); const newKey = 'my_new_key'; (actions.regenerateKey as SinonStub).resolves(newKey); @@ -98,7 +99,7 @@ describe('device-api/v1', () => { }); it('responds with 503 if regenerate was unsuccessful', async () => { - const oldKey = await deviceApi.getGlobalApiKey(); + const oldKey = await apiKeys.getGlobalApiKey(); (actions.regenerateKey as SinonStub).throws(new Error()); await request(api) @@ -125,7 +126,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/restart') .send({ appId: 1234567, force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doRestartStub).to.have.been.calledWith(1234567, false); doRestartStub.resetHistory(); @@ -134,7 +135,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/restart') .send({ appId: 7654321, force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doRestartStub).to.have.been.calledWith(7654321, true); doRestartStub.resetHistory(); @@ -143,7 +144,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/restart') .send({ appId: 7654321 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doRestartStub).to.have.been.calledWith(7654321, false); }); @@ -151,12 +152,12 @@ describe('device-api/v1', () => { it('responds with 400 if appId is missing', async () => { await request(api) .post('/v1/restart') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v1/restart') .send({ appId: 7654321 }) @@ -168,7 +169,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/restart') .send({ appId: 1234567 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); @@ -177,7 +178,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/restart') .send({ appId: 1234567 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -186,7 +187,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/restart') .send({ appId: 1234567 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -207,7 +208,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/purge') .send({ appId: 1234567, force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doPurgeStub).to.have.been.calledWith(1234567, false); doPurgeStub.resetHistory(); @@ -216,7 +217,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/purge') .send({ appId: 7654321, force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doPurgeStub).to.have.been.calledWith(7654321, true); doPurgeStub.resetHistory(); @@ -225,7 +226,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/purge') .send({ appId: 7654321 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doPurgeStub).to.have.been.calledWith(7654321, false); }); @@ -233,12 +234,12 @@ describe('device-api/v1', () => { it('responds with 400 if appId is missing', async () => { await request(api) .post('/v1/purge') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v1/purge') .send({ appId: 7654321 }) @@ -250,7 +251,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/purge') .send({ appId: 1234567 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); @@ -259,7 +260,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/purge') .send({ appId: 1234567 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -268,7 +269,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/purge') .send({ appId: 1234567 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -297,7 +298,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/apps/1234567/stop') .send({ force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -311,7 +312,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/apps/7654321/stop') .send({ force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -324,7 +325,7 @@ describe('device-api/v1', () => { // Defaults to force: false await request(api) .post('/v1/apps/7654321/stop') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -335,7 +336,7 @@ describe('device-api/v1', () => { }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v1/apps/7654321/stop') .set('Authorization', `Bearer ${scopedKey}`) @@ -345,7 +346,7 @@ describe('device-api/v1', () => { it('responds with 200 and containerId if service stop succeeded if service stop succeeded', async () => { await request(api) .post('/v1/apps/1234567/stop') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200, { containerId: 'abcdef' }); }); @@ -353,20 +354,20 @@ describe('device-api/v1', () => { executeServiceActionStub.throws(new NotFoundError()); await request(api) .post('/v1/apps/1234567/stop') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(404); }); it('responds with 400 if invalid appId or appId corresponds to a multicontainer release', async () => { await request(api) .post('/v1/apps/badAppId/stop') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); executeServiceActionStub.throws(new BadRequestError()); await request(api) .post('/v1/apps/1234567/stop') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); @@ -374,7 +375,7 @@ describe('device-api/v1', () => { executeServiceActionStub.throws(new UpdatesLockedError()); await request(api) .post('/v1/apps/1234567/stop') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -382,7 +383,7 @@ describe('device-api/v1', () => { executeServiceActionStub.throws(new Error()); await request(api) .post('/v1/apps/1234567/stop') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -411,7 +412,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/apps/1234567/start') .send({ force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -425,7 +426,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/apps/7654321/start') .send({ force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -438,7 +439,7 @@ describe('device-api/v1', () => { // Defaults to force: false await request(api) .post('/v1/apps/7654321/start') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -449,7 +450,7 @@ describe('device-api/v1', () => { }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v1/apps/7654321/start') .set('Authorization', `Bearer ${scopedKey}`) @@ -459,7 +460,7 @@ describe('device-api/v1', () => { it('responds with 200 and containerId if service start succeeded', async () => { await request(api) .post('/v1/apps/1234567/start') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200, { containerId: 'abcdef' }); }); @@ -467,20 +468,20 @@ describe('device-api/v1', () => { executeServiceActionStub.throws(new NotFoundError()); await request(api) .post('/v1/apps/1234567/start') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(404); }); it('responds with 400 if invalid appId or appId corresponds to a multicontainer release', async () => { await request(api) .post('/v1/apps/badAppId/start') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); executeServiceActionStub.throws(new BadRequestError()); await request(api) .post('/v1/apps/1234567/start') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); @@ -488,7 +489,7 @@ describe('device-api/v1', () => { executeServiceActionStub.throws(new UpdatesLockedError()); await request(api) .post('/v1/apps/1234567/start') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -496,7 +497,7 @@ describe('device-api/v1', () => { executeServiceActionStub.throws(new Error()); await request(api) .post('/v1/apps/1234567/start') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -513,7 +514,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/reboot') .send({ force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(executeDeviceActionStub).to.have.been.calledWith( { action: 'reboot', @@ -526,7 +527,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/reboot') .send({ force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(executeDeviceActionStub).to.have.been.calledWith( { action: 'reboot', @@ -538,7 +539,7 @@ describe('device-api/v1', () => { // Defaults to force: false await request(api) .post('/v1/reboot') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(executeDeviceActionStub).to.have.been.calledWith( { action: 'reboot', @@ -550,7 +551,7 @@ describe('device-api/v1', () => { it('responds with 202 if request successful', async () => { await request(api) .post('/v1/reboot') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(202); }); @@ -558,7 +559,7 @@ describe('device-api/v1', () => { executeDeviceActionStub.throws(new UpdatesLockedError()); await request(api) .post('/v1/reboot') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -566,7 +567,7 @@ describe('device-api/v1', () => { executeDeviceActionStub.throws(new Error()); await request(api) .post('/v1/reboot') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(500); }); }); @@ -583,7 +584,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/shutdown') .send({ force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(executeDeviceActionStub).to.have.been.calledWith( { action: 'shutdown', @@ -596,7 +597,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/shutdown') .send({ force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(executeDeviceActionStub).to.have.been.calledWith( { action: 'shutdown', @@ -608,7 +609,7 @@ describe('device-api/v1', () => { // Defaults to force: false await request(api) .post('/v1/shutdown') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(executeDeviceActionStub).to.have.been.calledWith( { action: 'shutdown', @@ -620,7 +621,7 @@ describe('device-api/v1', () => { it('responds with 202 if request successful', async () => { await request(api) .post('/v1/shutdown') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(202); }); @@ -628,7 +629,7 @@ describe('device-api/v1', () => { executeDeviceActionStub.throws(new UpdatesLockedError()); await request(api) .post('/v1/shutdown') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -636,7 +637,7 @@ describe('device-api/v1', () => { executeDeviceActionStub.throws(new Error()); await request(api) .post('/v1/shutdown') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(500); }); }); @@ -653,7 +654,7 @@ describe('device-api/v1', () => { await request(api) .post('/v1/update') .send({ force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(updateTargetStub.lastCall.firstArg).to.be.false; updateTargetStub.resetHistory(); @@ -661,14 +662,14 @@ describe('device-api/v1', () => { await request(api) .post('/v1/update') .send({ force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(updateTargetStub.lastCall.firstArg).to.be.true; updateTargetStub.resetHistory(); // Defaults to force: false await request(api) .post('/v1/update') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(updateTargetStub.lastCall.firstArg).to.be.false; }); @@ -676,7 +677,7 @@ describe('device-api/v1', () => { updateTargetStub.returns(true); await request(api) .post('/v1/update') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(204); }); @@ -684,7 +685,7 @@ describe('device-api/v1', () => { updateTargetStub.returns(false); await request(api) .post('/v1/update') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(202); }); }); @@ -706,21 +707,21 @@ describe('device-api/v1', () => { it('validates data from request body', async () => { await request(api) .get('/v1/apps/1234567') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`); + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`); expect(getSingleContainerAppStub).to.have.been.calledWith(1234567); }); it('responds with 200 if request successful', async () => { await request(api) .get('/v1/apps/1234567') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200, {}); }); it('responds with 400 if invalid appId parameter', async () => { await request(api) .get('/v1/apps/badAppId') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); @@ -728,12 +729,12 @@ describe('device-api/v1', () => { getSingleContainerAppStub.throws(new BadRequestError()); await request(api) .get('/v1/apps/1234567') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(7654321, 'main'); + const scopedKey = await apiKeys.generateScopedKey(7654321, 'main'); await request(api) .get('/v1/apps/1234567') .set('Authorization', `Bearer ${scopedKey}`) @@ -744,7 +745,7 @@ describe('device-api/v1', () => { getSingleContainerAppStub.throws(new Error()); await request(api) .get('/v1/apps/1234567') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -760,7 +761,7 @@ describe('device-api/v1', () => { getLegacyDeviceStateStub.resolves({ test_state: 'Success' }); await request(api) .get('/v1/device') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200, { test_state: 'Success' }); }); @@ -768,7 +769,7 @@ describe('device-api/v1', () => { getLegacyDeviceStateStub.throws(new Error()); await request(api) .get('/v1/device') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -787,7 +788,7 @@ describe('device-api/v1', () => { getHostConfigStub.resolves({ network: { hostname: 'deadbeef' } }); await request(api) .get('/v1/device/host-config') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200, { network: { hostname: 'deadbeef' } }); }); @@ -795,7 +796,7 @@ describe('device-api/v1', () => { getHostConfigStub.throws(new Error()); await request(api) .get('/v1/device/host-config') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -824,7 +825,7 @@ describe('device-api/v1', () => { for (const key of Object.keys(invalidProxyReqs)) { await request(api) .patch('/v1/device/host-config') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .send({ network: { proxy: { [key]: invalidProxyReqs[key] } } }) .expect(200) .then(() => { @@ -852,7 +853,7 @@ describe('device-api/v1', () => { await request(api) .patch('/v1/device/host-config') .send({}) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200) .then(() => { expect(log.warn as SinonStub).to.have.been.calledWith( diff --git a/test/integration/device-api/v2.spec.ts b/test/integration/device-api/v2.spec.ts index 98ba0db9..def56381 100644 --- a/test/integration/device-api/v2.spec.ts +++ b/test/integration/device-api/v2.spec.ts @@ -6,6 +6,7 @@ import request from 'supertest'; import * as config from '~/src/config'; import * as db from '~/src/db'; +import * as apiKeys from '~/lib/api-keys'; import * as deviceApi from '~/src/device-api'; import * as actions from '~/src/device-api/actions'; import * as v2 from '~/src/device-api/v2'; @@ -50,7 +51,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/restart') .send({ force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doRestartStub).to.have.been.calledWith(1234567, false); doRestartStub.resetHistory(); @@ -59,7 +60,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/restart') .send({ force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doRestartStub).to.have.been.calledWith(7654321, true); doRestartStub.resetHistory(); @@ -67,7 +68,7 @@ describe('device-api/v2', () => { // Defaults to force: false await request(api) .post('/v2/applications/7654321/restart') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doRestartStub).to.have.been.calledWith(7654321, false); }); @@ -75,12 +76,12 @@ describe('device-api/v2', () => { it('responds with 400 if appId is missing', async () => { await request(api) .post('/v2/applications/badAppId/restart') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v2/applications/7654321/restart') .set('Authorization', `Bearer ${scopedKey}`) @@ -90,7 +91,7 @@ describe('device-api/v2', () => { it('responds with 200 if restart succeeded', async () => { await request(api) .post('/v2/applications/1234567/restart') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); @@ -98,7 +99,7 @@ describe('device-api/v2', () => { doRestartStub.throws(new UpdatesLockedError()); await request(api) .post('/v2/applications/1234567/restart') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -106,7 +107,7 @@ describe('device-api/v2', () => { doRestartStub.throws(new Error()); await request(api) .post('/v2/applications/7654321/restart') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -128,7 +129,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/purge') .send({ force: false }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doPurgeStub).to.have.been.calledWith(1234567, false); doPurgeStub.resetHistory(); @@ -137,7 +138,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/purge') .send({ force: true }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doPurgeStub).to.have.been.calledWith(7654321, true); doPurgeStub.resetHistory(); @@ -145,7 +146,7 @@ describe('device-api/v2', () => { // Defaults to force: false await request(api) .post('/v2/applications/7654321/purge') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(doPurgeStub).to.have.been.calledWith(7654321, false); }); @@ -153,12 +154,12 @@ describe('device-api/v2', () => { it('responds with 400 if appId is missing', async () => { await request(api) .post('/v2/applications/badAppId/purge') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v2/applications/7654321/purge') .set('Authorization', `Bearer ${scopedKey}`) @@ -168,7 +169,7 @@ describe('device-api/v2', () => { it('responds with 200 if purge succeeded', async () => { await request(api) .post('/v2/applications/1234567/purge') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); @@ -176,7 +177,7 @@ describe('device-api/v2', () => { doPurgeStub.throws(new UpdatesLockedError()); await request(api) .post('/v2/applications/1234567/purge') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -184,7 +185,7 @@ describe('device-api/v2', () => { doPurgeStub.throws(new Error()); await request(api) .post('/v2/applications/7654321/purge') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -209,7 +210,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/stop-service') .send({ force: false, serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -224,7 +225,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/stop-service') .send({ force: true, serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -239,7 +240,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/stop-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -254,7 +255,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/stop-service') .send({ imageId: 111 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -269,7 +270,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/stop-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'stop', @@ -281,7 +282,7 @@ describe('device-api/v2', () => { }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v2/applications/7654321/stop-service') .send({ serviceName: 'test' }) @@ -293,7 +294,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/stop-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); @@ -302,21 +303,21 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/stop-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(404); }); it('responds with 400 if invalid appId or missing serviceName/imageId from request body', async () => { await request(api) .post('/v2/applications/badAppId/stop-service') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); executeServiceActionStub.throws(new BadRequestError()); await request(api) .post('/v2/applications/1234567/stop-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); @@ -325,7 +326,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/stop-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -334,7 +335,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/stop-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -359,7 +360,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/start-service') .send({ force: false, serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -374,7 +375,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/start-service') .send({ force: true, serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -389,7 +390,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/start-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -404,7 +405,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/start-service') .send({ imageId: 111 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -419,7 +420,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/start-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'start', @@ -431,7 +432,7 @@ describe('device-api/v2', () => { }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v2/applications/7654321/start-service') .send({ serviceName: 'test' }) @@ -443,7 +444,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/start-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); @@ -452,21 +453,21 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/start-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(404); }); it('responds with 400 if invalid appId or missing serviceName/imageId from request body', async () => { await request(api) .post('/v2/applications/badAppId/start-service') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); executeServiceActionStub.throws(new BadRequestError()); await request(api) .post('/v2/applications/1234567/start-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); @@ -475,7 +476,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/start-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -484,7 +485,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/start-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); @@ -509,7 +510,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/restart-service') .send({ force: false, serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'restart', @@ -524,7 +525,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/restart-service') .send({ force: true, serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'restart', @@ -539,7 +540,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/restart-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'restart', @@ -554,7 +555,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/restart-service') .send({ imageId: 111 }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'restart', @@ -569,7 +570,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/restart-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); expect(executeServiceActionStub).to.have.been.calledWith({ action: 'restart', @@ -581,7 +582,7 @@ describe('device-api/v2', () => { }); it("responds with 401 if caller's API key is not in scope of appId", async () => { - const scopedKey = await deviceApi.generateScopedKey(1234567, 'main'); + const scopedKey = await apiKeys.generateScopedKey(1234567, 'main'); await request(api) .post('/v2/applications/7654321/restart-service') .send({ serviceName: 'test' }) @@ -593,7 +594,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/restart-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); @@ -602,21 +603,21 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/restart-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(404); }); it('responds with 400 if invalid appId or missing serviceName/imageId from request body', async () => { await request(api) .post('/v2/applications/badAppId/restart-service') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); executeServiceActionStub.throws(new BadRequestError()); await request(api) .post('/v2/applications/1234567/restart-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(400); }); @@ -625,7 +626,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/1234567/restart-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(423); }); @@ -634,7 +635,7 @@ describe('device-api/v2', () => { await request(api) .post('/v2/applications/7654321/restart-service') .send({ serviceName: 'test' }) - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(503); }); }); diff --git a/test/legacy/42-device-api-v2.spec.ts b/test/legacy/42-device-api-v2.spec.ts index fa6ba459..afcb5829 100644 --- a/test/legacy/42-device-api-v2.spec.ts +++ b/test/legacy/42-device-api-v2.spec.ts @@ -8,7 +8,7 @@ import mockedAPI = require('~/test-lib/mocked-device-api'); import * as apiBinder from '~/src/api-binder'; import * as deviceState from '~/src/device-state'; import type SupervisorAPI from '~/src/device-api'; -import * as deviceApi from '~/src/device-api'; +import * as apiKeys from '~/lib/api-keys'; import * as serviceManager from '~/src/compose/service-manager'; import * as images from '~/src/compose/images'; import * as config from '~/src/config'; @@ -78,7 +78,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { await request .get('/v2/device/vpn') .set('Accept', 'application/json') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect('Content-Type', /json/) .expect(sampleResponses.V2.GET['/device/vpn'].statusCode) .then((response) => { @@ -94,7 +94,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { await request .get('/v2/applications/1/state') .set('Accept', 'application/json') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(sampleResponses.V2.GET['/applications/1/state'].statusCode) .expect('Content-Type', /json/) .then((response) => { @@ -108,7 +108,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { await request .get('/v2/applications/123invalid/state') .set('Accept', 'application/json') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect('Content-Type', /json/) .expect( sampleResponses.V2.GET['/applications/123invalid/state'].statusCode, @@ -124,7 +124,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { await request .get('/v2/applications/9000/state') .set('Accept', 'application/json') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(sampleResponses.V2.GET['/applications/9000/state'].statusCode) .then((response) => { expect(response.body).to.deep.equal( @@ -135,7 +135,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { describe('Scoped API Keys', () => { it('returns 409 because app is out of scope of the key', async () => { - const apiKey = await deviceApi.generateScopedKey(3, 'main'); + const apiKey = await apiKeys.generateScopedKey(3, 'main'); await request .get('/v2/applications/2/state') .set('Accept', 'application/json') @@ -157,7 +157,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { it('should return scoped application', async () => { // Create scoped key for application - const appScopedKey = await deviceApi.generateScopedKey(1658654, 'main'); + const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main'); // Setup device conditions serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]); imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]); @@ -181,7 +181,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { it('should return no application info due to lack of scope', async () => { // Create scoped key for wrong application - const appScopedKey = await deviceApi.generateScopedKey(1, 'main'); + const appScopedKey = await apiKeys.generateScopedKey(1, 'main'); // Setup device conditions serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]); imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]); @@ -204,7 +204,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { it('should return success when device has no applications', async () => { // Create scoped key for any application - const appScopedKey = await deviceApi.generateScopedKey(1658654, 'main'); + const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main'); // Setup device conditions serviceManagerMock.resolves([]); imagesMock.resolves([]); @@ -227,7 +227,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => { it('should only return 1 application when N > 1 applications on device', async () => { // Create scoped key for application - const appScopedKey = await deviceApi.generateScopedKey(1658654, 'main'); + const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main'); // Setup device conditions serviceManagerMock.resolves([ mockedAPI.mockService({ appId: 1658654 }), diff --git a/test/integration/device-api/api-keys.spec.ts b/test/lib/api-keys.spec.ts similarity index 72% rename from test/integration/device-api/api-keys.spec.ts rename to test/lib/api-keys.spec.ts index c51d4a21..7ef67043 100644 --- a/test/integration/device-api/api-keys.spec.ts +++ b/test/lib/api-keys.spec.ts @@ -4,9 +4,9 @@ import { expect } from 'chai'; import * as config from '~/src/config'; import * as testDb from '~/src/db'; -import * as deviceApi from '~/src/device-api'; +import * as apiKeys from '~/lib/api-keys'; import * as middleware from '~/src/device-api/middleware'; -import type { AuthorizedRequest } from '~/src/device-api/api-keys'; +import type { AuthorizedRequest } from '~/lib/api-keys'; describe('device-api/api-keys', () => { let app: express.Application; @@ -30,8 +30,8 @@ describe('device-api/api-keys', () => { }); it('should generate a key which is scoped for a single application', async () => { - const appOneKey = await deviceApi.generateScopedKey(111, 'one'); - const appTwoKey = await deviceApi.generateScopedKey(222, 'two'); + const appOneKey = await apiKeys.generateScopedKey(111, 'one'); + const appTwoKey = await apiKeys.generateScopedKey(222, 'two'); await request(app) .get('/test/111') @@ -55,7 +55,7 @@ describe('device-api/api-keys', () => { }); it('should generate a key which is scoped for multiple applications', async () => { - const multiAppKey = await deviceApi.generateScopedKey(111, 'three', { + const multiAppKey = await apiKeys.generateScopedKey(111, 'three', { scopes: [111, 222].map((appId) => ({ type: 'app', appId })), }); @@ -73,54 +73,54 @@ describe('device-api/api-keys', () => { it('should generate a key which is scoped for all applications', async () => { await request(app) .get('/test/111') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); await request(app) .get('/test/222') - .set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`) + .set('Authorization', `Bearer ${await apiKeys.getGlobalApiKey()}`) .expect(200); }); it('should have a cached lookup of key scopes', async () => { - const globalScopes = await deviceApi.getScopesForKey( - await deviceApi.getGlobalApiKey(), + const globalScopes = await apiKeys.getScopesForKey( + await apiKeys.getGlobalApiKey(), ); const key = 'my-new-key'; await testDb .models('apiSecret') - .where({ key: await deviceApi.getGlobalApiKey() }) + .where({ key: await apiKeys.getGlobalApiKey() }) .update({ key }); // Key has been changed, but cache should retain the old key expect( - await deviceApi.getScopesForKey(await deviceApi.getGlobalApiKey()), + await apiKeys.getScopesForKey(await apiKeys.getGlobalApiKey()), ).to.deep.equal(globalScopes); // Bust the cache and generate a new global API key - const refreshedKey = await deviceApi.refreshKey( - await deviceApi.getGlobalApiKey(), + const refreshedKey = await apiKeys.refreshKey( + await apiKeys.getGlobalApiKey(), ); // Key that we changed in db is no longer valid - expect(await deviceApi.getScopesForKey(key)).to.be.null; + expect(await apiKeys.getScopesForKey(key)).to.be.null; // Refreshed key should have the global scopes - expect(await deviceApi.getScopesForKey(refreshedKey)).to.deep.equal( + expect(await apiKeys.getScopesForKey(refreshedKey)).to.deep.equal( globalScopes, ); }); it('should regenerate a key and invalidate the old one', async () => { - const appScopedKey = await deviceApi.generateScopedKey(111, 'four'); + const appScopedKey = await apiKeys.generateScopedKey(111, 'four'); await request(app) .get('/test/111') .set('Authorization', `Bearer ${appScopedKey}`) .expect(200); - const newScopedKey = await deviceApi.refreshKey(appScopedKey); + const newScopedKey = await apiKeys.refreshKey(appScopedKey); await request(app) .get('/test/111') From 59689b27892a0bb3e6ec692c158070b6defb48a9 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Wed, 15 May 2024 16:39:33 -0400 Subject: [PATCH 6/7] Do not export balenaApi on api-binder Instead, add `getBalenaApi` function to api-helper so other modules can access a balena API instance. Further reduces circular dependencies to 5 Change-type: patch --- src/api-binder/index.ts | 23 ++++------------------- src/lib/api-helper.ts | 27 ++++++++++++++++++++++++++- src/lib/legacy.ts | 22 ++++------------------ src/supervisor.ts | 3 ++- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/api-binder/index.ts b/src/api-binder/index.ts index 6ede0f6d..3d4e1794 100644 --- a/src/api-binder/index.ts +++ b/src/api-binder/index.ts @@ -3,8 +3,7 @@ import { stripIndent } from 'common-tags'; import { isLeft } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; import _ from 'lodash'; -import { PinejsClientRequest } from 'pinejs-client-request'; -import url from 'url'; +import type { PinejsClientRequest } from 'pinejs-client-request'; import * as config from '../config'; import * as deviceConfig from '../device-config'; @@ -17,7 +16,6 @@ import { InternalInconsistencyError, TargetStateError, } from '../lib/errors'; -import * as request from '../lib/request'; import log from '../lib/supervisor-console'; @@ -511,31 +509,18 @@ async function reportInitialName( } } -export let balenaApi: PinejsClientRequest | null = null; +let balenaApi: PinejsClientRequest | null = null; export const initialized = _.once(async () => { await config.initialized(); await deviceState.initialized(); - const { unmanaged, apiEndpoint, currentApiKey } = await config.getMany([ - 'unmanaged', - 'apiEndpoint', - 'currentApiKey', - ]); + const unmanaged = await config.get('unmanaged'); if (unmanaged) { log.debug('Unmanaged mode is set, skipping API client initialization'); return; } - const baseUrl = url.resolve(apiEndpoint, '/v6/'); - const passthrough = structuredClone(await request.getRequestOptions()); - passthrough.headers = passthrough.headers != null ? passthrough.headers : {}; - passthrough.headers.Authorization = `Bearer ${currentApiKey}`; - balenaApi = new PinejsClientRequest({ - apiPrefix: baseUrl, - passthrough, - }); - - log.info(`API Binder bound to: ${baseUrl}`); + balenaApi = await apiHelper.getBalenaApi(); }); diff --git a/src/lib/api-helper.ts b/src/lib/api-helper.ts index 8aa4a074..0d2c3b33 100644 --- a/src/lib/api-helper.ts +++ b/src/lib/api-helper.ts @@ -1,4 +1,4 @@ -import type { PinejsClientRequest } from 'pinejs-client-request'; +import { PinejsClientRequest } from 'pinejs-client-request'; import Bluebird from 'bluebird'; import * as config from '../config'; @@ -14,6 +14,8 @@ import { isHttpConflictError, } from './errors'; import log from './supervisor-console'; +import memoizee from 'memoizee'; +import url from 'url'; export type KeyExchangeOpts = config.ConfigType<'provisioningOptions'>; @@ -23,6 +25,29 @@ export interface Device { [key: string]: unknown; } +export const getBalenaApi = memoizee( + async () => { + await config.initialized(); + + const { apiEndpoint, currentApiKey } = await config.getMany([ + 'apiEndpoint', + 'currentApiKey', + ]); + + const baseUrl = url.resolve(apiEndpoint, '/v6/'); + const passthrough = structuredClone(await request.getRequestOptions()); + passthrough.headers = + passthrough.headers != null ? passthrough.headers : {}; + passthrough.headers.Authorization = `Bearer ${currentApiKey}`; + log.info(`API Binder bound to: ${baseUrl}`); + return new PinejsClientRequest({ + apiPrefix: baseUrl, + passthrough, + }); + }, + { promise: true }, +); + export const fetchDevice = async ( balenaApi: PinejsClientRequest, uuid: string, diff --git a/src/lib/legacy.ts b/src/lib/legacy.ts index 12d20207..886a4954 100644 --- a/src/lib/legacy.ts +++ b/src/lib/legacy.ts @@ -1,5 +1,4 @@ import * as path from 'path'; -import * as apiBinder from '../api-binder'; import * as config from '../config'; import * as db from '../db'; import * as volumeManager from '../compose/volume-manager'; @@ -20,6 +19,7 @@ import type { DatabaseApp, DatabaseService, } from '../device-state/target-state-cache'; +import { getBalenaApi } from '../lib/api-helper'; import type { TargetApp, TargetApps, TargetState } from '../types'; @@ -54,13 +54,7 @@ async function createVolumeFromLegacyData( * Gets proper database ids from the cloud for the app and app services */ export async function normaliseLegacyDatabase() { - await apiBinder.initialized(); - - if (apiBinder.balenaApi == null) { - throw new InternalInconsistencyError( - 'API binder is not initialized correctly', - ); - } + const balenaApi = await getBalenaApi(); // When legacy apps are present, we kill their containers and migrate their /data to a named volume log.info('Migrating ids for legacy app...'); @@ -97,7 +91,7 @@ export async function normaliseLegacyDatabase() { } log.debug(`Getting release ${app.commit} for app ${app.appId} from API`); - const releases = await apiBinder.balenaApi.get({ + const releases = await balenaApi.get({ resource: 'release', options: { $filter: { @@ -266,15 +260,7 @@ export async function fromV2TargetState( * @param appId */ const getUUIDFromAPI = async (appId: number) => { - await apiBinder.initialized(); - - if (apiBinder.balenaApi == null) { - throw new InternalInconsistencyError( - 'API binder is not initialized correctly', - ); - } - - const { balenaApi } = apiBinder; + const balenaApi = await getBalenaApi(); const appDetails = await balenaApi.get({ resource: 'application', diff --git a/src/supervisor.ts b/src/supervisor.ts index fbe46fa9..3e7b4e51 100644 --- a/src/supervisor.ts +++ b/src/supervisor.ts @@ -58,8 +58,9 @@ export class Supervisor { await deviceState.initialized(); + const unmanaged = await config.get('unmanaged'); logger.logSystemMessage('Supervisor starting', {}, 'Supervisor start'); - if (conf.legacyAppsPresent && apiBinder.balenaApi != null) { + if (conf.legacyAppsPresent && !unmanaged) { log.info('Legacy app detected, running migration'); await normaliseLegacyDatabase(); } From 027c2575b1147b5395c61e3d824f9dff95e7f1d0 Mon Sep 17 00:00:00 2001 From: Felipe Lalanne <1822826+pipex@users.noreply.github.com> Date: Thu, 16 May 2024 11:28:41 -0400 Subject: [PATCH 7/7] Move OS variant retrieval to config module This also deprecates the `getOSVariant` function of the `os-release` module, as the OS variant are no longer defined in `/etc/os-release`. Change-type: patch --- src/config/functions.ts | 9 +++++++-- src/lib/os-release.ts | 12 +++++++----- src/lib/request.ts | 11 ++++++----- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/config/functions.ts b/src/config/functions.ts index fa5a6c51..0c98fc92 100644 --- a/src/config/functions.ts +++ b/src/config/functions.ts @@ -31,8 +31,13 @@ export const fnSchema = { osVersion: () => { return osRelease.getOSVersion(constants.hostOSVersionPath); }, - osVariant: () => { - return osRelease.getOSVariant(constants.hostOSVersionPath); + osVariant: async () => { + const osVariant = await osRelease.getOSVariant(constants.hostOSVersionPath); + if (osVariant === undefined) { + const developmentMode = await config.get('developmentMode'); + return developmentMode === true ? 'dev' : 'prod'; + } + return osVariant; }, macAddress: () => { return macAddress.getAll(constants.macAddressPath); diff --git a/src/lib/os-release.ts b/src/lib/os-release.ts index 40a8e6f4..1b3d8147 100644 --- a/src/lib/os-release.ts +++ b/src/lib/os-release.ts @@ -4,7 +4,6 @@ import { promises as fs } from 'fs'; import { InternalInconsistencyError } from './errors'; import { exec } from './fs-utils'; import log from './supervisor-console'; -import * as conf from '../config'; // Retrieve the data for the OS once only per path const getOSReleaseData = _.memoize( @@ -53,12 +52,15 @@ export async function getOSVersion(path: string): Promise { return getOSReleaseField(path, 'PRETTY_NAME'); } +/** + * Returns the OS variant information from /etc/release + * + * OS variants no longer exist and this function only exists for legacy reasons + * + * @deprecated + */ export async function getOSVariant(path: string): Promise { const osVariant = await getOSReleaseField(path, 'VARIANT_ID'); - if (osVariant === undefined) { - const developmentMode = await conf.get('developmentMode'); - return developmentMode === true ? 'dev' : 'prod'; - } return osVariant; } diff --git a/src/lib/request.ts b/src/lib/request.ts index 76eae460..42f86a4f 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -2,9 +2,7 @@ import Bluebird from 'bluebird'; import once = require('lodash/once'); import requestLib from 'request'; import resumableRequestLib from 'resumable-request'; - -import * as constants from './constants'; -import * as osRelease from './os-release'; +import * as config from '../config'; import supervisorVersion = require('./supervisor-version'); @@ -41,9 +39,12 @@ type PromisifiedRequest = typeof requestLib & { }; const getRequestInstances = once(async () => { + await config.initialized(); // Generate the user agents with out versions - const osVersion = await osRelease.getOSVersion(constants.hostOSVersionPath); - const osVariant = await osRelease.getOSVariant(constants.hostOSVersionPath); + const { osVersion, osVariant } = await config.getMany([ + 'osVersion', + 'osVariant', + ]); let userAgent = `Supervisor/${supervisorVersion}`; if (osVersion != null) { if (osVariant != null) {