Merge pull request #2334 from balena-os/circular-dependencies

Refactor code to reduce circular dependencies between modules
This commit is contained in:
flowzone-app[bot] 2024-05-27 20:03:42 +00:00 committed by GitHub
commit 1264012fad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 625 additions and 497 deletions

View File

@ -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();
});

View File

@ -2,15 +2,10 @@ 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 {
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,24 +44,12 @@ export interface AppConstructOpts {
networks: Network[];
}
export interface UpdateState {
availableImages: Image[];
containerIds: Dictionary<string>;
downloading: string[];
locksTaken: LocksTakenMap;
force: boolean;
}
interface ChangingPair<T> {
current?: T;
target?: T;
}
export interface AppsToLockMap {
[appId: number]: Set<string>;
}
export class App {
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 +1019,7 @@ export class App {
}),
);
return new App(
return new AppImpl(
{
appId: app.appId,
appUuid: app.uuid,
@ -1044,4 +1036,4 @@ export class App {
}
}
export default App;
export const App = AppImpl;

View File

@ -20,27 +20,30 @@ import {
import { getServicesLockedByAppId, LocksTakenMap } from '../lib/update-lock';
import { checkTruthy } from '../lib/validation';
import App from './app';
import type { UpdateState } from './app';
import { App } from './app';
import * as volumeManager from './volume-manager';
import * as networkManager from './network-manager';
import * as serviceManager from './service-manager';
import * as imageManager from './images';
import * as commitStore from './commit';
import type Service from './service';
import type Network from './network';
import type Volume from './volume';
import { generateStep, getExecutors } from './composition-steps';
import type {
InstancedAppState,
TargetApps,
DeviceLegacyReport,
AppState,
ServiceState,
} from '../types/state';
import type { Image } from './images';
import type { CompositionStep, CompositionStepT } from './composition-steps';
} from '../types';
import type {
CompositionStep,
CompositionStepT,
UpdateState,
Service,
Network,
Volume,
Image,
InstancedAppState,
} from './types';
type ApplicationManagerEventEmitter = StrictEventEmitter<
EventEmitter,

View File

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

View File

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

View File

@ -11,12 +11,15 @@ import type {
ComposeNetworkConfig,
NetworkConfig,
NetworkInspectInfo,
} from './types/network';
Network as NetworkIface,
} from './types';
import { InvalidNetworkNameError } from './errors';
import { InternalInconsistencyError } from '../lib/errors';
export class Network {
export type Network = NetworkIface;
class NetworkImpl implements Network {
public appId: number;
public appUuid?: string;
public name: string;
@ -303,4 +306,4 @@ export class Network {
}
}
export default Network;
export const Network = NetworkImpl;

View File

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

View File

@ -23,7 +23,9 @@ import type {
ConfigMap,
DeviceMetadata,
DockerDevice,
} from './types/service';
ServiceStatus,
Service as ServiceIface,
} from './types';
import {
ShortMount,
ShortBind,
@ -34,25 +36,16 @@ import {
LongBind,
LongAnonymousVolume,
LongNamedVolume,
} from './types/service';
} from './types';
const SERVICE_NETWORK_MODE_REGEX = /service:\s*(.+)/;
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 type Service = ServiceIface;
export class Service {
class ServiceImpl implements Service {
public appId: number;
public appUuid?: string;
public imageId: number;
@ -64,17 +57,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 +79,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 +93,7 @@ export class Service {
// These fields are special case, due to network_mode:service:<service>
'networkMode',
'hostname',
].concat(Service.allConfigArrayFields);
].concat(ServiceImpl.allConfigArrayFields);
private constructor() {
/* do not allow instancing a service object with `new` */
@ -1170,4 +1154,4 @@ export class Service {
}
}
export default Service;
export const Service = ServiceImpl;

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

@ -0,0 +1,38 @@
import type { Network } from './network';
import type { Volume } from './volume';
import type { Service } from './service';
import type { LocksTakenMap } from '../../lib/update-lock';
import type { Image } from './image';
import type { CompositionStep } from './composition-step';
export interface UpdateState {
availableImages: Image[];
containerIds: Dictionary<string>;
downloading: string[];
locksTaken: LocksTakenMap;
force: boolean;
}
export interface App {
appId: number;
appUuid?: string;
// When setting up an application from current state, these values are not available
appName?: string;
commit?: string;
source?: string;
isHost?: boolean;
// Services are stored as an array, as at any one time we could have more than one
// service for a single service ID running (for example handover)
services: Service[];
networks: Network[];
volumes: Volume[];
nextStepsForAppUpdate(state: UpdateState, target: App): CompositionStep[];
stepsToRemoveApp(
state: Omit<UpdateState, 'availableImages'> & { keepVolumes: boolean },
): CompositionStep[];
}
export interface AppsToLockMap {
[appId: number]: Set<string>;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,31 +1,16 @@
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`.
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<string>;
ipam: {
driver: string;
config: Array<
Partial<{
subnet: string;
ip_range: string;
gateway: string;
aux_addresses: Dictionary<string>;
}>
>;
options: Dictionary<string>;
};
enable_ipv6: boolean;
internal: boolean;
labels: Dictionary<string>;
config_only: boolean;
}
export interface NetworkConfig {
driver: string;
ipam: {
@ -44,3 +29,18 @@ export interface NetworkConfig {
options: { [optName: string]: string };
configOnly: boolean;
}
export interface Network {
appId: number;
appUuid?: string;
name: string;
config: NetworkConfig;
isEqualConfig(network: Network): boolean;
create(): Promise<void>;
remove(): Promise<void>;
toDockerConfig(): NetworkCreateOptions & {
ConfigOnly: boolean;
};
toComposeObject(): ComposeNetworkConfig;
}

View File

@ -336,3 +336,58 @@ export interface DockerDevice {
PathInContainer: string;
CgroupPermissions: string;
}
export type ServiceStatus =
| 'Stopping'
| 'Running'
| 'Installing'
| 'Installed'
| 'Dead'
| 'paused'
| 'restarting'
| 'removing'
| 'exited';
export interface Service {
appId: number;
appUuid?: string;
imageId: number;
config: ServiceConfig;
serviceName: string;
commit: string;
releaseId: number;
serviceId: number;
imageName: string | null;
containerId: string | null;
exitErrorMessage: string | null;
dependsOn: string[] | null;
dockerImageId: string | null;
// This looks weird, and it is. The lowercase statuses come from Docker,
// except the dashboard takes these values and displays them on the dashboard.
// What we should be doin is defining these container statuses, and have the
// dashboard make these human readable instead. Until that happens we have
// this halfways state of some captalised statuses, and others coming directly
// from docker
status: ServiceStatus;
createdAt: Date | null;
hasNetwork(networkName: string): boolean;
hasVolume(volumeName: string): boolean;
isEqualExceptForRunningState(
service: Service,
currentContainerIds: Dictionary<string>,
): boolean;
isEqualConfig(
service: Service,
currentContainerIds: Dictionary<string>,
): boolean;
hasNetworkMode(networkName: string): boolean;
extraNetworksToJoin(): ServiceConfig['networks'];
toDockerContainer(opts: {
deviceName: string;
containerIds: Dictionary<string>;
}): Dockerode.ContainerCreateOptions;
handoverCompleteFullPathsOnHost(): string[];
}

View File

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

View File

@ -1,8 +1,8 @@
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';
import type { AppsToLockMap } from './types';
import { InternalInconsistencyError } from '../lib/errors';
import { checkString } from '../lib/validation';

View File

@ -5,7 +5,6 @@ import { parse as parseCommand } from 'shell-quote';
import * as constants from '../lib/constants';
import { checkTruthy } from '../lib/validation';
import type { Service } from './service';
import type {
ComposeHealthcheck,
ConfigMap,
@ -16,11 +15,12 @@ import type {
ServiceHealthcheck,
LongDefinition,
LongBind,
} from './types/service';
Service,
} from './types';
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,
);

View File

@ -10,8 +10,8 @@ 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 Volume from './volume';
import type { VolumeConfig } from './types';
import { Volume } from './volume';
export interface VolumeNameOpts {
name: string;

View File

@ -10,19 +10,15 @@ import type { LabelObject } from '../types';
import * as logger from '../logger';
import * as ComposeUtils from './utils';
export interface VolumeConfig {
labels: LabelObject;
driver: string;
driverOpts: Docker.VolumeInspectInfo['Options'];
}
import type {
Volume as VolumeIface,
VolumeConfig,
ComposeVolumeConfig,
} from './types';
export interface ComposeVolumeConfig {
driver: string;
driver_opts: Dictionary<string>;
labels: LabelObject;
}
export type Volume = VolumeIface;
export class Volume {
class VolumeImpl implements Volume {
private constructor(
public name: string,
public appId: number,
@ -162,4 +158,4 @@ export class Volume {
}
}
export default Volume;
export const Volume = VolumeImpl;

View File

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

View File

@ -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';
@ -11,7 +11,7 @@ import * as applicationManager from '../compose/application-manager';
import type { CompositionStepAction } from '../compose/composition-steps';
import { generateStep } from '../compose/composition-steps';
import * as commitStore from '../compose/commit';
import type Service from '../compose/service';
import type { Service } from '../compose/service';
import { getApp } from '../device-state/db-format';
import * as TargetState from '../device-state/target-state';
import log from '../lib/supervisor-console';

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import * as apiBinder from '../api-binder';
import * as applicationManager from '../compose/application-manager';
import type { CompositionStepAction } from '../compose/composition-steps';
import type { Service } from '../compose/service';
import Volume from '../compose/volume';
import { Volume } from '../compose/volume';
import * as commitStore from '../compose/commit';
import * as config from '../config';
import * as db from '../db';
@ -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';

View File

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

View File

@ -4,16 +4,16 @@ 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 {
InstancedAppState,
TargetApp,
TargetApps,
TargetRelease,
TargetService,
} from '../types/state';
import type { InstancedAppState } from '../compose/types';
type InstancedApp = InstancedAppState[0];

View File

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

View File

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

View File

@ -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';
@ -14,12 +13,13 @@ 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,
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',

View File

@ -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<string | undefined> {
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<string | undefined> {
const osVariant = await getOSReleaseField(path, 'VARIANT_ID');
if (osVariant === undefined) {
const developmentMode = await conf.get('developmentMode');
return developmentMode === true ? 'dev' : 'prod';
}
return osVariant;
}

View File

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

View File

@ -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();
}

View File

@ -1,9 +1,6 @@
import * as t from 'io-ts';
// TODO: move all these exported types to ../compose/types
import type { ComposeNetworkConfig } from '../compose/types/network';
import type { ComposeVolumeConfig } from '../compose/volume';
import type { ContractObject } from '../lib/contracts';
import type { ContractObject } from '@balena/contrato';
import {
DockerName,
@ -16,7 +13,32 @@ import {
nonEmptyRecord,
} from './basic';
import type App from '../compose/app';
export interface ComposeVolumeConfig {
driver: string;
driver_opts: Dictionary<string>;
labels: LabelObject;
}
export interface ComposeNetworkConfig {
driver: string;
driver_opts: Dictionary<string>;
ipam: {
driver: string;
config: Array<
Partial<{
subnet: string;
ip_range: string;
gateway: string;
aux_addresses: Dictionary<string>;
}>
>;
options: Dictionary<string>;
};
enable_ipv6: boolean;
internal: boolean;
labels: Dictionary<string>;
config_only: boolean;
}
export type DeviceLegacyReport = Partial<{
api_port: number;
@ -356,13 +378,3 @@ export const AppsJsonFormat = t.intersection([
t.partial({ pinDevice: t.boolean }),
]);
export type AppsJsonFormat = t.TypeOf<typeof AppsJsonFormat>;
export type InstancedAppState = { [appId: number]: App };
export interface InstancedDeviceState {
local: {
name: string;
config: Dictionary<string>;
apps: InstancedAppState;
};
}

View File

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

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import Service from '~/src/compose/service';
import * as deviceApi from '~/src/device-api';
import { Service } from '~/src/compose/service';
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(
{

View File

@ -2,7 +2,7 @@ import { expect } from 'chai';
import * as sinon from 'sinon';
import * as volumeManager from '~/src/compose/volume-manager';
import Volume from '~/src/compose/volume';
import { Volume } from '~/src/compose/volume';
import { createDockerImage } from '~/test-lib/docker-helper';
import Docker from 'dockerode';

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import type { SinonStub } from 'sinon';
import { stub } from 'sinon';
import Volume from '~/src/compose/volume';
import { Volume } from '~/src/compose/volume';
import * as logTypes from '~/lib/log-types';
import * as logger from '~/src/logger';

View File

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

View File

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

View File

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

View File

@ -7,7 +7,8 @@ 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 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(

View File

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

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { isRight } from 'fp-ts/lib/Either';
import App from '~/src/compose/app';
import Network from '~/src/compose/network';
import { App } from '~/src/compose/app';
import { Network } from '~/src/compose/network';
import * as config from '~/src/config';
import * as testDb from '~/src/db';
import * as dbFormat from '~/src/device-state/db-format';

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
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,
} 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',

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import type { Image } from '~/src/compose/images';
import Network from '~/src/compose/network';
import Volume from '~/src/compose/volume';
import { Network } from '~/src/compose/network';
import { Volume } from '~/src/compose/volume';
import { LocksTakenMap } from '~/lib/update-lock';
import {

View File

@ -2,7 +2,7 @@ import { expect } from 'chai';
import type * as sinon from 'sinon';
import { Network } from '~/src/compose/network';
import type { NetworkInspectInfo } from '~/src/compose/types/network';
import type { NetworkInspectInfo } from '~/src/compose/types';
import { log } from '~/lib/supervisor-console';

View File

@ -3,8 +3,8 @@ import * as _ from 'lodash';
import { expect } from 'chai';
import { createContainer } from '~/test-lib/mockerode';
import Service from '~/src/compose/service';
import Volume from '~/src/compose/volume';
import { Service } from '~/src/compose/service';
import { Volume } from '~/src/compose/volume';
import * as ServiceT from '~/src/compose/types/service';
import * as constants from '~/lib/constants';

View File

@ -1,5 +1,5 @@
import { expect } from 'chai';
import Volume from '~/src/compose/volume';
import { Volume } from '~/src/compose/volume';
describe('compose/volume: unit tests', () => {
describe('creating a volume from a compose object', () => {