mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-05 17:11:39 +00:00
Merge pull request #1842 from balena-os/api-code-reorganization
Move all Supervisor API related routes & code to device-api directory
This commit is contained in:
commit
ccb04d42ce
@ -1,6 +1,5 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
import { stripIndent } from 'common-tags';
|
import { stripIndent } from 'common-tags';
|
||||||
import * as express from 'express';
|
|
||||||
import { isLeft } from 'fp-ts/lib/Either';
|
import { isLeft } from 'fp-ts/lib/Either';
|
||||||
import * as t from 'io-ts';
|
import * as t from 'io-ts';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
@ -45,6 +44,10 @@ interface DeviceTag {
|
|||||||
|
|
||||||
let readyForUpdates = false;
|
let readyForUpdates = false;
|
||||||
|
|
||||||
|
export function isReadyForUpdates() {
|
||||||
|
return readyForUpdates;
|
||||||
|
}
|
||||||
|
|
||||||
export async function healthcheck() {
|
export async function healthcheck() {
|
||||||
const { appUpdatePollInterval, unmanaged, connectivityCheckEnabled } =
|
const { appUpdatePollInterval, unmanaged, connectivityCheckEnabled } =
|
||||||
await config.getMany([
|
await config.getMany([
|
||||||
@ -570,29 +573,3 @@ export const initialized = _.once(async () => {
|
|||||||
|
|
||||||
log.info(`API Binder bound to: ${baseUrl}`);
|
log.info(`API Binder bound to: ${baseUrl}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const router = express.Router();
|
|
||||||
router.use(express.urlencoded({ limit: '10mb', extended: true }));
|
|
||||||
router.use(express.json({ limit: '10mb' }));
|
|
||||||
|
|
||||||
router.post('/v1/update', (req, res, next) => {
|
|
||||||
eventTracker.track('Update notification');
|
|
||||||
if (readyForUpdates) {
|
|
||||||
config
|
|
||||||
.get('instantUpdates')
|
|
||||||
.then((instantUpdates) => {
|
|
||||||
if (instantUpdates) {
|
|
||||||
TargetState.update(req.body.force, true).catch(_.noop);
|
|
||||||
res.sendStatus(204);
|
|
||||||
} else {
|
|
||||||
log.debug(
|
|
||||||
'Ignoring update notification because instant updates are disabled',
|
|
||||||
);
|
|
||||||
res.sendStatus(202);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(next);
|
|
||||||
} else {
|
|
||||||
res.sendStatus(202);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
@ -59,7 +59,6 @@ export class App {
|
|||||||
public commit?: string;
|
public commit?: string;
|
||||||
public source?: string;
|
public source?: string;
|
||||||
public isHost?: boolean;
|
public isHost?: boolean;
|
||||||
|
|
||||||
// Services are stored as an array, as at any one time we could have more than one
|
// 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)
|
// service for a single service ID running (for example handover)
|
||||||
public services: Service[];
|
public services: Service[];
|
||||||
|
@ -1,48 +1,45 @@
|
|||||||
import * as express from 'express';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
import StrictEventEmitter from 'strict-event-emitter-types';
|
import StrictEventEmitter from 'strict-event-emitter-types';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import { transaction, Transaction } from '../db';
|
import { transaction, Transaction } from '../db';
|
||||||
|
import * as logger from '../logger';
|
||||||
|
import LocalModeManager from '../local-mode';
|
||||||
|
import proxyvisor from '../proxyvisor';
|
||||||
|
|
||||||
import * as dbFormat from '../device-state/db-format';
|
import * as dbFormat from '../device-state/db-format';
|
||||||
import { validateTargetContracts } from '../lib/contracts';
|
import { validateTargetContracts } from '../lib/contracts';
|
||||||
import constants = require('../lib/constants');
|
import constants = require('../lib/constants');
|
||||||
import { docker } from '../lib/docker-utils';
|
import { docker } from '../lib/docker-utils';
|
||||||
import * as logger from '../logger';
|
|
||||||
import log from '../lib/supervisor-console';
|
import log from '../lib/supervisor-console';
|
||||||
import LocalModeManager from '../local-mode';
|
|
||||||
import {
|
import {
|
||||||
ContractViolationError,
|
ContractViolationError,
|
||||||
InternalInconsistencyError,
|
InternalInconsistencyError,
|
||||||
} from '../lib/errors';
|
} from '../lib/errors';
|
||||||
import { lock } from '../lib/update-lock';
|
import { lock } from '../lib/update-lock';
|
||||||
|
import { checkTruthy } from '../lib/validation';
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
import * as volumeManager from './volume-manager';
|
import * as volumeManager from './volume-manager';
|
||||||
import * as networkManager from './network-manager';
|
import * as networkManager from './network-manager';
|
||||||
import * as serviceManager from './service-manager';
|
import * as serviceManager from './service-manager';
|
||||||
import * as imageManager from './images';
|
import * as imageManager from './images';
|
||||||
import type { Image } from './images';
|
|
||||||
import { getExecutors, CompositionStepT } from './composition-steps';
|
|
||||||
import * as commitStore from './commit';
|
import * as commitStore from './commit';
|
||||||
|
|
||||||
import Service from './service';
|
import Service from './service';
|
||||||
import Network from './network';
|
import Network from './network';
|
||||||
import Volume from './volume';
|
import Volume from './volume';
|
||||||
|
import { generateStep, getExecutors } from './composition-steps';
|
||||||
|
|
||||||
import { createV1Api } from '../device-api/v1';
|
import type {
|
||||||
import { createV2Api } from '../device-api/v2';
|
|
||||||
import { CompositionStep, generateStep } from './composition-steps';
|
|
||||||
import {
|
|
||||||
InstancedAppState,
|
InstancedAppState,
|
||||||
TargetApps,
|
TargetApps,
|
||||||
DeviceLegacyReport,
|
DeviceLegacyReport,
|
||||||
AppState,
|
AppState,
|
||||||
ServiceState,
|
ServiceState,
|
||||||
} from '../types/state';
|
} from '../types/state';
|
||||||
import { checkTruthy } from '../lib/validation';
|
import type { Image } from './images';
|
||||||
import { Proxyvisor } from '../proxyvisor';
|
import type { CompositionStep, CompositionStepT } from './composition-steps';
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
|
|
||||||
type ApplicationManagerEventEmitter = StrictEventEmitter<
|
type ApplicationManagerEventEmitter = StrictEventEmitter<
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@ -56,22 +53,8 @@ export const removeListener: typeof events['removeListener'] =
|
|||||||
export const removeAllListeners: typeof events['removeAllListeners'] =
|
export const removeAllListeners: typeof events['removeAllListeners'] =
|
||||||
events.removeAllListeners.bind(events);
|
events.removeAllListeners.bind(events);
|
||||||
|
|
||||||
const proxyvisor = new Proxyvisor();
|
|
||||||
const localModeManager = new LocalModeManager();
|
const localModeManager = new LocalModeManager();
|
||||||
|
|
||||||
export const router = (() => {
|
|
||||||
const $router = express.Router();
|
|
||||||
$router.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
||||||
$router.use(express.json({ limit: '10mb' }));
|
|
||||||
|
|
||||||
createV1Api($router);
|
|
||||||
createV2Api($router);
|
|
||||||
|
|
||||||
$router.use(proxyvisor.router);
|
|
||||||
|
|
||||||
return $router;
|
|
||||||
})();
|
|
||||||
|
|
||||||
export let fetchesInProgress = 0;
|
export let fetchesInProgress = 0;
|
||||||
export let timeSpentFetching = 0;
|
export let timeSpentFetching = 0;
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
|
|
||||||
import log from '../lib/supervisor-console';
|
import log from '../lib/supervisor-console';
|
||||||
|
|
||||||
import * as apiKeys from '../lib/api-keys';
|
import * as deviceApi from '../device-api';
|
||||||
|
|
||||||
export function camelCaseConfig(
|
export function camelCaseConfig(
|
||||||
literalConfig: ConfigMap,
|
literalConfig: ConfigMap,
|
||||||
@ -377,7 +377,7 @@ export async function addFeaturesFromLabels(
|
|||||||
},
|
},
|
||||||
'io.balena.features.supervisor-api': async () => {
|
'io.balena.features.supervisor-api': async () => {
|
||||||
// create a app/service specific API secret
|
// create a app/service specific API secret
|
||||||
const apiSecret = await apiKeys.generateScopedKey(
|
const apiSecret = await deviceApi.generateScopedKey(
|
||||||
service.appId,
|
service.appId,
|
||||||
service.serviceName,
|
service.serviceName,
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as memoizee from 'memoizee';
|
import * as memoizee from 'memoizee';
|
||||||
|
import { TypedError } from 'typed-error';
|
||||||
|
|
||||||
import * as config from '../config';
|
|
||||||
import * as db from '../db';
|
import * as db from '../db';
|
||||||
|
|
||||||
import { generateUniqueKey } from './register-device';
|
import { generateUniqueKey } from '../lib/register-device';
|
||||||
|
|
||||||
export class KeyNotFoundError extends Error {}
|
class KeyNotFoundError extends TypedError {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The schema for the `apiSecret` table in the database
|
* The schema for the `apiSecret` table in the database
|
||||||
@ -20,7 +20,7 @@ interface DbApiSecret {
|
|||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Scope = SerializableScope<ScopeTypeKey>;
|
type Scope = SerializableScope<ScopeTypeKey>;
|
||||||
type ScopeTypeKey = keyof ScopeTypes;
|
type ScopeTypeKey = keyof ScopeTypes;
|
||||||
type SerializableScope<T extends ScopeTypeKey> = {
|
type SerializableScope<T extends ScopeTypeKey> = {
|
||||||
type: T;
|
type: T;
|
||||||
@ -59,13 +59,8 @@ const scopeChecks: ScopeCheckCollection = {
|
|||||||
resources.apps != null && resources.apps.includes(appId),
|
resources.apps != null && resources.apps.includes(appId),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function serialiseScopes(scopes: Scope[]): string {
|
const serialiseScopes = (scopes: Scope[]): string => JSON.stringify(scopes);
|
||||||
return JSON.stringify(scopes);
|
const deserialiseScopes = (json: string): Scope[] => JSON.parse(json);
|
||||||
}
|
|
||||||
|
|
||||||
export function deserialiseScopes(json: string): Scope[] {
|
|
||||||
return JSON.parse(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isScoped = (
|
export const isScoped = (
|
||||||
resources: Partial<ScopedResources>,
|
resources: Partial<ScopedResources>,
|
||||||
@ -88,106 +83,28 @@ export type AuthorizedRequestHandler = (
|
|||||||
next: express.NextFunction,
|
next: express.NextFunction,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
// empty until populated in `initialized`
|
|
||||||
export let cloudApiKey: string = '';
|
|
||||||
|
|
||||||
// should be called before trying to use this singleton
|
// should be called before trying to use this singleton
|
||||||
export const initialized = _.once(async () => {
|
export const initialized = _.once(async () => {
|
||||||
await db.initialized();
|
await db.initialized();
|
||||||
|
|
||||||
// make sure we have an API key which the cloud will use to call us
|
// make sure we have an API key which the cloud will use to call us
|
||||||
await generateCloudKey();
|
await generateGlobalKey();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// empty until populated in `initialized`
|
||||||
* This middleware will extract an API key used to make a call, and then expand it out to provide
|
let globalApiKey: string = '';
|
||||||
* access to the scopes it has. The `req` will be updated to include this `auth` data.
|
|
||||||
*
|
|
||||||
* E.g. `req.auth.scopes: []`
|
|
||||||
*
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @param next
|
|
||||||
*/
|
|
||||||
export const authMiddleware: AuthorizedRequestHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
next,
|
|
||||||
) => {
|
|
||||||
// grab the API key used for the request
|
|
||||||
const apiKey = getApiKeyFromRequest(req) ?? '';
|
|
||||||
|
|
||||||
// store the key in the request, and an empty scopes array to populate after resolving the key scopes
|
export const getGlobalApiKey = async (): Promise<string> => {
|
||||||
req.auth = {
|
if (globalApiKey === '') {
|
||||||
apiKey,
|
await initialized();
|
||||||
scopes: [],
|
|
||||||
isScoped: (resources) => isScoped(resources, req.auth.scopes),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const conf = await config.getMany(['localMode', 'unmanaged']);
|
|
||||||
|
|
||||||
// we only need to check the API key if managed and not in local mode
|
|
||||||
const needsAuth = !conf.unmanaged && !conf.localMode;
|
|
||||||
|
|
||||||
// no need to authenticate, shortcut
|
|
||||||
if (!needsAuth) {
|
|
||||||
// Allow requests that do not need auth to be scoped for all applications
|
|
||||||
req.auth.isScoped = () => true;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have a key, find the scopes and add them to the request
|
|
||||||
if (apiKey && apiKey !== '') {
|
|
||||||
await initialized();
|
|
||||||
const scopes = await getScopesForKey(apiKey);
|
|
||||||
|
|
||||||
if (scopes != null) {
|
|
||||||
// keep the scopes for later incase they're desired
|
|
||||||
req.auth.scopes.push(...scopes);
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we do not have a valid key...
|
|
||||||
return res.sendStatus(401);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(503).send(`Unexpected error: ${err}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return globalApiKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isEqualScope(a: Scope, b: Scope): boolean {
|
const isEqualScope = (a: Scope, b: Scope): boolean => _.isEqual(a, b);
|
||||||
return _.isEqual(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getApiKeyFromRequest(req: express.Request): string | undefined {
|
type GenerateKeyOptions = { force: boolean; scopes: Scope[] };
|
||||||
// Check query for key
|
|
||||||
if (req.query.apikey) {
|
|
||||||
if (typeof req.query.apikey !== 'string') {
|
|
||||||
// We were passed something as an api key but it wasn't a string
|
|
||||||
// so ignore it
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return req.query.apikey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Authorization header to search for key
|
|
||||||
const authHeader = req.get('Authorization');
|
|
||||||
|
|
||||||
// Check header for key
|
|
||||||
if (!authHeader) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check authHeader with various schemes
|
|
||||||
const match = authHeader.match(/^(?:ApiKey|Bearer) (\w+)$/i);
|
|
||||||
|
|
||||||
// Return key from match or undefined
|
|
||||||
return match?.[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GenerateKeyOptions = { force: boolean; scopes: Scope[] };
|
|
||||||
|
|
||||||
export async function getScopesForKey(key: string): Promise<Scope[] | null> {
|
export async function getScopesForKey(key: string): Promise<Scope[] | null> {
|
||||||
const apiKey = await getApiKeyByKey(key);
|
const apiKey = await getApiKeyByKey(key);
|
||||||
@ -209,14 +126,12 @@ export async function generateScopedKey(
|
|||||||
return await generateKey(appId, serviceName, options);
|
return await generateKey(appId, serviceName, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateCloudKey(
|
async function generateGlobalKey(force: boolean = false): Promise<string> {
|
||||||
force: boolean = false,
|
globalApiKey = await generateKey(0, null, {
|
||||||
): Promise<string> {
|
|
||||||
cloudApiKey = await generateKey(0, null, {
|
|
||||||
force,
|
force,
|
||||||
scopes: [{ type: 'global' }],
|
scopes: [{ type: 'global' }],
|
||||||
});
|
});
|
||||||
return cloudApiKey;
|
return globalApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshKey(key: string): Promise<string> {
|
export async function refreshKey(key: string): Promise<string> {
|
||||||
@ -230,7 +145,7 @@ export async function refreshKey(key: string): Promise<string> {
|
|||||||
|
|
||||||
// if this is a cloud key that is being refreshed
|
// if this is a cloud key that is being refreshed
|
||||||
if (appId === 0 && serviceName === null) {
|
if (appId === 0 && serviceName === null) {
|
||||||
return await generateCloudKey(true);
|
return await generateGlobalKey(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate a new key, expiring the old one...
|
// generate a new key, expiring the old one...
|
@ -6,11 +6,14 @@ import * as deviceState from '../device-state';
|
|||||||
import * as applicationManager from '../compose/application-manager';
|
import * as applicationManager from '../compose/application-manager';
|
||||||
import * as serviceManager from '../compose/service-manager';
|
import * as serviceManager from '../compose/service-manager';
|
||||||
import * as volumeManager from '../compose/volume-manager';
|
import * as volumeManager from '../compose/volume-manager';
|
||||||
|
import { App } from '../compose/app';
|
||||||
import { InternalInconsistencyError } from '../lib/errors';
|
import { InternalInconsistencyError } from '../lib/errors';
|
||||||
import { lock } from '../lib/update-lock';
|
import { lock } from '../lib/update-lock';
|
||||||
import { appNotFoundMessage } from '../lib/messages';
|
import { appNotFoundMessage } from './messages';
|
||||||
|
|
||||||
export async function doRestart(appId, force) {
|
import type { InstancedDeviceState } from '../types';
|
||||||
|
|
||||||
|
export async function doRestart(appId: number, force: boolean) {
|
||||||
await deviceState.initialized();
|
await deviceState.initialized();
|
||||||
await applicationManager.initialized();
|
await applicationManager.initialized();
|
||||||
|
|
||||||
@ -37,7 +40,7 @@ export async function doRestart(appId, force) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doPurge(appId, force) {
|
export async function doPurge(appId: number, force: boolean) {
|
||||||
await deviceState.initialized();
|
await deviceState.initialized();
|
||||||
await applicationManager.initialized();
|
await applicationManager.initialized();
|
||||||
|
|
||||||
@ -120,10 +123,10 @@ export async function doPurge(appId, force) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This doesn't truly return an InstancedDeviceState, but it's close enough to mostly work where it's used
|
* This doesn't truly return an InstancedDeviceState, but it's close enough to mostly work where it's used
|
||||||
*
|
|
||||||
* @returns { import('../types/state').InstancedDeviceState }
|
|
||||||
*/
|
*/
|
||||||
export function safeStateClone(targetState) {
|
export function safeStateClone(
|
||||||
|
targetState: InstancedDeviceState,
|
||||||
|
): InstancedDeviceState {
|
||||||
// We avoid using cloneDeep here, as the class
|
// We avoid using cloneDeep here, as the class
|
||||||
// instances can cause a maximum call stack exceeded
|
// instances can cause a maximum call stack exceeded
|
||||||
// error
|
// error
|
||||||
@ -136,8 +139,7 @@ export function safeStateClone(targetState) {
|
|||||||
// thing to do would be to represent the input with
|
// thing to do would be to represent the input with
|
||||||
// io-ts and make sure the below conforms to it
|
// io-ts and make sure the below conforms to it
|
||||||
|
|
||||||
/** @type { any } */
|
const cloned: DeepPartial<InstancedDeviceState> = {
|
||||||
const cloned = {
|
|
||||||
local: {
|
local: {
|
||||||
config: {},
|
config: {},
|
||||||
},
|
},
|
||||||
@ -157,43 +159,48 @@ export function safeStateClone(targetState) {
|
|||||||
cloned.dependent = _.cloneDeep(targetState.dependent);
|
cloned.dependent = _.cloneDeep(targetState.dependent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cloned;
|
return cloned as InstancedDeviceState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeAppClone(app) {
|
export function safeAppClone(app: App): App {
|
||||||
const containerIdForService = _.fromPairs(
|
const containerIdForService = _.fromPairs(
|
||||||
_.map(app.services, (svc) => [
|
_.map(app.services, (svc) => [
|
||||||
svc.serviceName,
|
svc.serviceName,
|
||||||
svc.containerId != null ? svc.containerId.substr(0, 12) : '',
|
svc.containerId != null ? svc.containerId.substring(0, 12) : '',
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
return {
|
return new App(
|
||||||
appId: app.appId,
|
{
|
||||||
name: app.name,
|
appId: app.appId,
|
||||||
commit: app.commit,
|
appUuid: app.appUuid,
|
||||||
releaseId: app.releaseId,
|
appName: app.appName,
|
||||||
services: _.map(app.services, (svc) => {
|
commit: app.commit,
|
||||||
// This is a bit of a hack, but when applying the target state as if it's
|
source: app.source,
|
||||||
// the current state, this will include the previous containerId as a
|
services: _.map(app.services, (svc) => {
|
||||||
// network alias. The container ID will be there as Docker adds it
|
// This is a bit of a hack, but when applying the target state as if it's
|
||||||
// implicitly when creating a container. Here, we remove any previous
|
// the current state, this will include the previous containerId as a
|
||||||
// container IDs before passing it back as target state. We have to do this
|
// network alias. The container ID will be there as Docker adds it
|
||||||
// here as when passing it back as target state, the service class cannot
|
// implicitly when creating a container. Here, we remove any previous
|
||||||
// know that the alias being given is not in fact a user given one.
|
// container IDs before passing it back as target state. We have to do this
|
||||||
// TODO: Make the process of moving from a current state to a target state
|
// here as when passing it back as target state, the service class cannot
|
||||||
// well-defined (and implemented in a separate module)
|
// know that the alias being given is not in fact a user given one.
|
||||||
const svcCopy = _.cloneDeep(svc);
|
// TODO: Make the process of moving from a current state to a target state
|
||||||
|
// well-defined (and implemented in a separate module)
|
||||||
|
const svcCopy = _.cloneDeep(svc);
|
||||||
|
|
||||||
_.each(svcCopy.config.networks, (net) => {
|
_.each(svcCopy.config.networks, (net) => {
|
||||||
if (Array.isArray(net.aliases)) {
|
if (Array.isArray(net.aliases)) {
|
||||||
net.aliases = net.aliases.filter(
|
net.aliases = net.aliases.filter(
|
||||||
(alias) => alias !== containerIdForService[svcCopy.serviceName],
|
(alias) => alias !== containerIdForService[svcCopy.serviceName],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return svcCopy;
|
return svcCopy;
|
||||||
}),
|
}),
|
||||||
volumes: _.cloneDeep(app.volumes),
|
volumes: _.cloneDeep(app.volumes),
|
||||||
networks: _.cloneDeep(app.networks),
|
networks: _.cloneDeep(app.networks),
|
||||||
};
|
isHost: app.isHost,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
@ -0,0 +1,140 @@
|
|||||||
|
import * as express from 'express';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import * as middleware from './middleware';
|
||||||
|
import * as apiKeys from './api-keys';
|
||||||
|
import * as eventTracker from '../event-tracker';
|
||||||
|
import { reportCurrentState } from '../device-state';
|
||||||
|
import proxyvisor from '../proxyvisor';
|
||||||
|
import blink = require('../lib/blink');
|
||||||
|
import log from '../lib/supervisor-console';
|
||||||
|
|
||||||
|
import type { Server } from 'http';
|
||||||
|
|
||||||
|
interface SupervisorAPIConstructOpts {
|
||||||
|
routers: express.Router[];
|
||||||
|
healthchecks: Array<() => Promise<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SupervisorAPIStopOpts {
|
||||||
|
errored: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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[];
|
||||||
|
private healthchecks: Array<() => Promise<boolean>>;
|
||||||
|
|
||||||
|
private api = express();
|
||||||
|
private server: Server | null = null;
|
||||||
|
|
||||||
|
public constructor({ routers, healthchecks }: SupervisorAPIConstructOpts) {
|
||||||
|
this.routers = routers;
|
||||||
|
this.healthchecks = healthchecks;
|
||||||
|
|
||||||
|
this.api.disable('x-powered-by');
|
||||||
|
this.api.use(middleware.logging);
|
||||||
|
|
||||||
|
this.api.get('/v1/healthy', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const healths = await Promise.all(this.healthchecks.map((fn) => fn()));
|
||||||
|
if (!_.every(healths)) {
|
||||||
|
log.error('Healthcheck failed');
|
||||||
|
return res.status(500).send('Unhealthy');
|
||||||
|
}
|
||||||
|
return res.sendStatus(200);
|
||||||
|
} catch {
|
||||||
|
log.error('Healthcheck failed');
|
||||||
|
return res.status(500).send('Unhealthy');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.get('/ping', (_req, res) => res.send('OK'));
|
||||||
|
|
||||||
|
this.api.use(middleware.auth);
|
||||||
|
|
||||||
|
this.api.post('/v1/blink', (_req, res) => {
|
||||||
|
eventTracker.track('Device blink');
|
||||||
|
blink.pattern.start();
|
||||||
|
setTimeout(blink.pattern.stop, 15000);
|
||||||
|
return res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expires the supervisor's API key and generates a new one.
|
||||||
|
// It also communicates the new key to the balena API.
|
||||||
|
this.api.post(
|
||||||
|
'/v1/regenerate-api-key',
|
||||||
|
async (req: apiKeys.AuthorizedRequest, res) => {
|
||||||
|
await apiKeys.initialized();
|
||||||
|
|
||||||
|
// check if we're updating the cloud API key
|
||||||
|
const shouldUpdateCloudKey =
|
||||||
|
req.auth.apiKey === (await getGlobalApiKey());
|
||||||
|
|
||||||
|
// regenerate the key...
|
||||||
|
const newKey = await apiKeys.refreshKey(req.auth.apiKey);
|
||||||
|
|
||||||
|
// if we need to update the cloud API with our new key
|
||||||
|
if (shouldUpdateCloudKey) {
|
||||||
|
// report the new key to the cloud API
|
||||||
|
reportCurrentState({
|
||||||
|
api_secret: newKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the value of the new key to the caller
|
||||||
|
res.status(200).send(newKey);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.api.use(express.urlencoded({ limit: '10mb', extended: true }));
|
||||||
|
this.api.use(express.json({ limit: '10mb' }));
|
||||||
|
|
||||||
|
// And assign all external routers
|
||||||
|
for (const router of this.routers) {
|
||||||
|
this.api.use(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.api.use(proxyvisor.router);
|
||||||
|
|
||||||
|
this.api.use(middleware.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listen(port: number, apiTimeout: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.server = this.api.listen(port, () => {
|
||||||
|
log.info(`Supervisor API successfully started on port ${port}`);
|
||||||
|
if (this.server) {
|
||||||
|
this.server.timeout = apiTimeout;
|
||||||
|
}
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(options?: SupervisorAPIStopOpts): Promise<void> {
|
||||||
|
if (this.server != null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server?.close((err: Error) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Failed to stop Supervisor API');
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
options?.errored
|
||||||
|
? log.error('Stopped Supervisor API')
|
||||||
|
: log.info('Stopped Supervisor API');
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SupervisorAPI;
|
84
src/device-api/middleware/auth.ts
Normal file
84
src/device-api/middleware/auth.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import * as apiKeys from '../api-keys';
|
||||||
|
import * as config from '../../config';
|
||||||
|
|
||||||
|
import type { Request } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This middleware will extract an API key used to make a call, and then expand it out to provide
|
||||||
|
* access to the scopes it has. The `req` will be updated to include this `auth` data.
|
||||||
|
*
|
||||||
|
* E.g. `req.auth.scopes: []`
|
||||||
|
*
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
export const auth: apiKeys.AuthorizedRequestHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
next,
|
||||||
|
) => {
|
||||||
|
// grab the API key used for the request
|
||||||
|
const apiKey = getApiKeyFromRequest(req) ?? '';
|
||||||
|
|
||||||
|
// store the key in the request, and an empty scopes array to populate after resolving the key scopes
|
||||||
|
req.auth = {
|
||||||
|
apiKey,
|
||||||
|
scopes: [],
|
||||||
|
isScoped: (resources) => apiKeys.isScoped(resources, req.auth.scopes),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conf = await config.getMany(['localMode', 'unmanaged']);
|
||||||
|
|
||||||
|
// we only need to check the API key if managed and not in local mode
|
||||||
|
const needsAuth = !conf.unmanaged && !conf.localMode;
|
||||||
|
|
||||||
|
// no need to authenticate, shortcut
|
||||||
|
if (!needsAuth) {
|
||||||
|
// Allow requests that do not need auth to be scoped for all applications
|
||||||
|
req.auth.isScoped = () => true;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a key, find the scopes and add them to the request
|
||||||
|
if (apiKey && apiKey !== '') {
|
||||||
|
await apiKeys.initialized();
|
||||||
|
const scopes = await apiKeys.getScopesForKey(apiKey);
|
||||||
|
|
||||||
|
if (scopes != null) {
|
||||||
|
// keep the scopes for later incase they're desired
|
||||||
|
req.auth.scopes.push(...scopes);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not have a valid key...
|
||||||
|
return res.sendStatus(401);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(503).send(`Unexpected error: ${err}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getApiKeyFromRequest(req: Request): string | undefined {
|
||||||
|
const { apikey } = req.query;
|
||||||
|
// Check query for key
|
||||||
|
if (apikey && typeof apikey === 'string') {
|
||||||
|
return apikey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Authorization header to search for key
|
||||||
|
const authHeader = req.get('Authorization');
|
||||||
|
|
||||||
|
// Check header for key
|
||||||
|
if (!authHeader) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authHeader with various schemes
|
||||||
|
const match = authHeader.match(/^(?:ApiKey|Bearer) (\w+)$/i);
|
||||||
|
|
||||||
|
// Return key from match or undefined
|
||||||
|
return match?.[1];
|
||||||
|
}
|
42
src/device-api/middleware/errors.ts
Normal file
42
src/device-api/middleware/errors.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import { UpdatesLockedError } from '../../lib/errors';
|
||||||
|
import log from '../../lib/supervisor-console';
|
||||||
|
|
||||||
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
const messageFromError = (err?: Error | string | null): string => {
|
||||||
|
let message = 'Unknown error';
|
||||||
|
if (err != null) {
|
||||||
|
if (_.isError(err) && err.message != null) {
|
||||||
|
message = err.message;
|
||||||
|
} else {
|
||||||
|
message = err as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const errors = (
|
||||||
|
err: Error,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) => {
|
||||||
|
if (res.headersSent) {
|
||||||
|
// Error happens while we are writing the response - default handler closes the connection.
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 423 Locked when locks as set
|
||||||
|
const code = err instanceof UpdatesLockedError ? 423 : 503;
|
||||||
|
if (code !== 423) {
|
||||||
|
log.error(`Error on ${req.method} ${req.path}: `, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(code).send({
|
||||||
|
status: 'failed',
|
||||||
|
message: messageFromError(err),
|
||||||
|
});
|
||||||
|
};
|
3
src/device-api/middleware/index.ts
Normal file
3
src/device-api/middleware/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './logging';
|
||||||
|
export * from './auth';
|
||||||
|
export * from './errors';
|
19
src/device-api/middleware/logging.ts
Normal file
19
src/device-api/middleware/logging.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import * as morgan from 'morgan';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
import log from '../../lib/supervisor-console';
|
||||||
|
|
||||||
|
export const logging = morgan(
|
||||||
|
(tokens, req: Request, res) =>
|
||||||
|
[
|
||||||
|
tokens.method(req, res),
|
||||||
|
req.path,
|
||||||
|
tokens.status(req, res),
|
||||||
|
'-',
|
||||||
|
tokens['response-time'](req, res),
|
||||||
|
'ms',
|
||||||
|
].join(' '),
|
||||||
|
{
|
||||||
|
stream: { write: (d) => log.api(d.toString().trimEnd()) },
|
||||||
|
},
|
||||||
|
);
|
@ -1,181 +1,170 @@
|
|||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import { doRestart, doPurge } from './common';
|
||||||
|
import { AuthorizedRequest } from './api-keys';
|
||||||
import * as eventTracker from '../event-tracker';
|
import * as eventTracker from '../event-tracker';
|
||||||
|
import { isReadyForUpdates } from '../api-binder';
|
||||||
|
import * as config from '../config';
|
||||||
|
import * as deviceState from '../device-state';
|
||||||
|
|
||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
import { checkInt, checkTruthy } from '../lib/validation';
|
import { checkInt, checkTruthy } from '../lib/validation';
|
||||||
import { doRestart, doPurge } from './common';
|
import log from '../lib/supervisor-console';
|
||||||
|
import { UpdatesLockedError } from '../lib/errors';
|
||||||
|
import * as hostConfig from '../host-config';
|
||||||
import * as applicationManager from '../compose/application-manager';
|
import * as applicationManager from '../compose/application-manager';
|
||||||
import { generateStep } from '../compose/composition-steps';
|
import { generateStep } from '../compose/composition-steps';
|
||||||
import * as commitStore from '../compose/commit';
|
import * as commitStore from '../compose/commit';
|
||||||
import { AuthorizedRequest } from '../lib/api-keys';
|
|
||||||
import { getApp } from '../device-state/db-format';
|
import { getApp } from '../device-state/db-format';
|
||||||
|
import * as TargetState from '../device-state/target-state';
|
||||||
|
|
||||||
export function createV1Api(router: express.Router) {
|
const disallowedHostConfigPatchFields = ['local_ip', 'local_port'];
|
||||||
router.post('/v1/restart', (req: AuthorizedRequest, res, next) => {
|
|
||||||
const appId = checkInt(req.body.appId);
|
|
||||||
const force = checkTruthy(req.body.force);
|
|
||||||
eventTracker.track('Restart container (v1)', { appId });
|
|
||||||
if (appId == null) {
|
|
||||||
return res.status(400).send('Missing app id');
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the case where the appId is out of scope
|
export const router = express.Router();
|
||||||
if (!req.auth.isScoped({ apps: [appId] })) {
|
|
||||||
res.status(401).json({
|
|
||||||
status: 'failed',
|
|
||||||
message: 'Application is not available',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return doRestart(appId, force)
|
router.post('/v1/restart', (req: AuthorizedRequest, res, next) => {
|
||||||
.then(() => res.status(200).send('OK'))
|
const appId = checkInt(req.body.appId);
|
||||||
.catch(next);
|
const force = checkTruthy(req.body.force);
|
||||||
});
|
eventTracker.track('Restart container (v1)', { appId });
|
||||||
|
if (appId == null) {
|
||||||
|
return res.status(400).send('Missing app id');
|
||||||
|
}
|
||||||
|
|
||||||
const v1StopOrStart = (
|
// handle the case where the appId is out of scope
|
||||||
req: AuthorizedRequest,
|
if (!req.auth.isScoped({ apps: [appId] })) {
|
||||||
res: express.Response,
|
res.status(401).json({
|
||||||
next: express.NextFunction,
|
status: 'failed',
|
||||||
action: 'start' | 'stop',
|
message: 'Application is not available',
|
||||||
) => {
|
});
|
||||||
const appId = checkInt(req.params.appId);
|
return;
|
||||||
const force = checkTruthy(req.body.force);
|
}
|
||||||
if (appId == null) {
|
|
||||||
return res.status(400).send('Missing app id');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all([applicationManager.getCurrentApps(), getApp(appId)])
|
return doRestart(appId, force)
|
||||||
.then(([apps, targetApp]) => {
|
.then(() => res.status(200).send('OK'))
|
||||||
if (apps[appId] == null) {
|
.catch(next);
|
||||||
return res.status(400).send('App not found');
|
});
|
||||||
}
|
|
||||||
const app = apps[appId];
|
|
||||||
let service = app.services[0];
|
|
||||||
if (service == null) {
|
|
||||||
return res.status(400).send('No services on app');
|
|
||||||
}
|
|
||||||
if (app.services.length > 1) {
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.send(
|
|
||||||
'Some v1 endpoints are only allowed on single-container apps',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that the request is scoped to cover this application
|
const v1StopOrStart = (
|
||||||
if (!req.auth.isScoped({ apps: [app.appId] })) {
|
req: AuthorizedRequest,
|
||||||
return res.status(401).send('Unauthorized');
|
res: express.Response,
|
||||||
}
|
next: express.NextFunction,
|
||||||
|
action: 'start' | 'stop',
|
||||||
|
) => {
|
||||||
|
const appId = checkInt(req.params.appId);
|
||||||
|
const force = checkTruthy(req.body.force);
|
||||||
|
if (appId == null) {
|
||||||
|
return res.status(400).send('Missing app id');
|
||||||
|
}
|
||||||
|
|
||||||
// Get the service from the target state (as we do in v2)
|
return Promise.all([applicationManager.getCurrentApps(), getApp(appId)])
|
||||||
// TODO: what if we want to start a service belonging to the current app?
|
.then(([apps, targetApp]) => {
|
||||||
const targetService = _.find(targetApp.services, {
|
if (apps[appId] == null) {
|
||||||
serviceName: service.serviceName,
|
|
||||||
});
|
|
||||||
|
|
||||||
applicationManager.setTargetVolatileForService(service.imageId, {
|
|
||||||
running: action !== 'stop',
|
|
||||||
});
|
|
||||||
|
|
||||||
const stopOpts = { wait: true };
|
|
||||||
const step = generateStep(action, {
|
|
||||||
current: service,
|
|
||||||
target: targetService,
|
|
||||||
...stopOpts,
|
|
||||||
});
|
|
||||||
|
|
||||||
return applicationManager
|
|
||||||
.executeStep(step, { force })
|
|
||||||
.then(function () {
|
|
||||||
if (action === 'stop') {
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
// We refresh the container id in case we were starting an app with no container yet
|
|
||||||
return applicationManager.getCurrentApps().then(function (apps2) {
|
|
||||||
const app2 = apps2[appId];
|
|
||||||
service = app2.services[0];
|
|
||||||
if (service == null) {
|
|
||||||
throw new Error('App not found after running action');
|
|
||||||
}
|
|
||||||
return service;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((service2) =>
|
|
||||||
res.status(200).json({ containerId: service2.containerId }),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createV1StopOrStartHandler = (action: 'start' | 'stop') =>
|
|
||||||
_.partial(v1StopOrStart, _, _, _, action);
|
|
||||||
|
|
||||||
router.post('/v1/apps/:appId/stop', createV1StopOrStartHandler('stop'));
|
|
||||||
router.post('/v1/apps/:appId/start', createV1StopOrStartHandler('start'));
|
|
||||||
|
|
||||||
router.get('/v1/apps/:appId', async (req: AuthorizedRequest, res, next) => {
|
|
||||||
const appId = checkInt(req.params.appId);
|
|
||||||
eventTracker.track('GET app (v1)', { appId });
|
|
||||||
if (appId == null) {
|
|
||||||
return res.status(400).send('Missing app id');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const apps = await applicationManager.getCurrentApps();
|
|
||||||
const app = apps[appId];
|
|
||||||
const service = app?.services?.[0];
|
|
||||||
if (service == null) {
|
|
||||||
return res.status(400).send('App not found');
|
return res.status(400).send('App not found');
|
||||||
}
|
}
|
||||||
|
const app = apps[appId];
|
||||||
// handle the case where the appId is out of scope
|
let service = app.services[0];
|
||||||
if (!req.auth.isScoped({ apps: [app.appId] })) {
|
if (service == null) {
|
||||||
res.status(401).json({
|
return res.status(400).send('No services on app');
|
||||||
status: 'failed',
|
|
||||||
message: 'Application is not available',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.services.length > 1) {
|
if (app.services.length > 1) {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.send('Some v1 endpoints are only allowed on single-container apps');
|
.send('Some v1 endpoints are only allowed on single-container apps');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because we only have a single app, we can fetch the commit for that
|
// check that the request is scoped to cover this application
|
||||||
// app, and maintain backwards compatability
|
if (!req.auth.isScoped({ apps: [app.appId] })) {
|
||||||
const commit = await commitStore.getCommitForApp(appId);
|
return res.status(401).send('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
// Don't return data that will be of no use to the user
|
// Get the service from the target state (as we do in v2)
|
||||||
const appToSend = {
|
// TODO: what if we want to start a service belonging to the current app?
|
||||||
appId,
|
const targetService = _.find(targetApp.services, {
|
||||||
commit,
|
serviceName: service.serviceName,
|
||||||
containerId: service.containerId,
|
});
|
||||||
env: _.omit(service.config.environment, constants.privateAppEnvVars),
|
|
||||||
imageId: service.config.image,
|
|
||||||
releaseId: service.releaseId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return res.json(appToSend);
|
applicationManager.setTargetVolatileForService(service.imageId, {
|
||||||
} catch (e) {
|
running: action !== 'stop',
|
||||||
next(e);
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/v1/purge', (req: AuthorizedRequest, res, next) => {
|
const stopOpts = { wait: true };
|
||||||
const appId = checkInt(req.body.appId);
|
const step = generateStep(action, {
|
||||||
const force = checkTruthy(req.body.force);
|
current: service,
|
||||||
if (appId == null) {
|
target: targetService,
|
||||||
const errMsg = 'Invalid or missing appId';
|
...stopOpts,
|
||||||
return res.status(400).send(errMsg);
|
});
|
||||||
|
|
||||||
|
return applicationManager
|
||||||
|
.executeStep(step, { force })
|
||||||
|
.then(function () {
|
||||||
|
if (action === 'stop') {
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
// We refresh the container id in case we were starting an app with no container yet
|
||||||
|
return applicationManager.getCurrentApps().then(function (apps2) {
|
||||||
|
const app2 = apps2[appId];
|
||||||
|
service = app2.services[0];
|
||||||
|
if (service == null) {
|
||||||
|
throw new Error('App not found after running action');
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((service2) =>
|
||||||
|
res.status(200).json({ containerId: service2.containerId }),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createV1StopOrStartHandler = (action: 'start' | 'stop') =>
|
||||||
|
_.partial(v1StopOrStart, _, _, _, action);
|
||||||
|
|
||||||
|
router.post('/v1/apps/:appId/stop', createV1StopOrStartHandler('stop'));
|
||||||
|
router.post('/v1/apps/:appId/start', createV1StopOrStartHandler('start'));
|
||||||
|
|
||||||
|
const rebootOrShutdown = async (
|
||||||
|
req: express.Request,
|
||||||
|
res: express.Response,
|
||||||
|
action: deviceState.DeviceStateStepTarget,
|
||||||
|
) => {
|
||||||
|
const override = await config.get('lockOverride');
|
||||||
|
const force = checkTruthy(req.body.force) || override;
|
||||||
|
try {
|
||||||
|
const response = await deviceState.executeStepAction({ action }, { force });
|
||||||
|
res.status(202).json(response);
|
||||||
|
} catch (e: any) {
|
||||||
|
const status = e instanceof UpdatesLockedError ? 423 : 500;
|
||||||
|
res.status(status).json({
|
||||||
|
Data: '',
|
||||||
|
Error: (e != null ? e.message : undefined) || e || 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/v1/reboot', (req, res) => rebootOrShutdown(req, res, 'reboot'));
|
||||||
|
router.post('/v1/shutdown', (req, res) =>
|
||||||
|
rebootOrShutdown(req, res, 'shutdown'),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get('/v1/apps/:appId', async (req: AuthorizedRequest, res, next) => {
|
||||||
|
const appId = checkInt(req.params.appId);
|
||||||
|
eventTracker.track('GET app (v1)', { appId });
|
||||||
|
if (appId == null) {
|
||||||
|
return res.status(400).send('Missing app id');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apps = await applicationManager.getCurrentApps();
|
||||||
|
const app = apps[appId];
|
||||||
|
const service = app?.services?.[0];
|
||||||
|
if (service == null) {
|
||||||
|
return res.status(400).send('App not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the case where the appId is out of scope
|
// handle the case where the appId is out of scope
|
||||||
if (!req.auth.isScoped({ apps: [appId] })) {
|
if (!req.auth.isScoped({ apps: [app.appId] })) {
|
||||||
res.status(401).json({
|
res.status(401).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: 'Application is not available',
|
message: 'Application is not available',
|
||||||
@ -183,8 +172,183 @@ export function createV1Api(router: express.Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return doPurge(appId, force)
|
if (app.services.length > 1) {
|
||||||
.then(() => res.status(200).json({ Data: 'OK', Error: '' }))
|
return res
|
||||||
|
.status(400)
|
||||||
|
.send('Some v1 endpoints are only allowed on single-container apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because we only have a single app, we can fetch the commit for that
|
||||||
|
// app, and maintain backwards compatability
|
||||||
|
const commit = await commitStore.getCommitForApp(appId);
|
||||||
|
|
||||||
|
// Don't return data that will be of no use to the user
|
||||||
|
const appToSend = {
|
||||||
|
appId,
|
||||||
|
commit,
|
||||||
|
containerId: service.containerId,
|
||||||
|
env: _.omit(service.config.environment, constants.privateAppEnvVars),
|
||||||
|
imageId: service.config.image,
|
||||||
|
releaseId: service.releaseId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json(appToSend);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/v1/purge', (req: AuthorizedRequest, res, next) => {
|
||||||
|
const appId = checkInt(req.body.appId);
|
||||||
|
const force = checkTruthy(req.body.force);
|
||||||
|
if (appId == null) {
|
||||||
|
const errMsg = 'Invalid or missing appId';
|
||||||
|
return res.status(400).send(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the case where the appId is out of scope
|
||||||
|
if (!req.auth.isScoped({ apps: [appId] })) {
|
||||||
|
res.status(401).json({
|
||||||
|
status: 'failed',
|
||||||
|
message: 'Application is not available',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return doPurge(appId, force)
|
||||||
|
.then(() => res.status(200).json({ Data: 'OK', Error: '' }))
|
||||||
|
.catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/v1/update', (req, res, next) => {
|
||||||
|
eventTracker.track('Update notification');
|
||||||
|
if (isReadyForUpdates()) {
|
||||||
|
config
|
||||||
|
.get('instantUpdates')
|
||||||
|
.then((instantUpdates) => {
|
||||||
|
if (instantUpdates) {
|
||||||
|
TargetState.update(req.body.force, true).catch(_.noop);
|
||||||
|
res.sendStatus(204);
|
||||||
|
} else {
|
||||||
|
log.debug(
|
||||||
|
'Ignoring update notification because instant updates are disabled',
|
||||||
|
);
|
||||||
|
res.sendStatus(202);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(next);
|
.catch(next);
|
||||||
});
|
} else {
|
||||||
}
|
res.sendStatus(202);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/v1/device/host-config', (_req, res) =>
|
||||||
|
hostConfig
|
||||||
|
.get()
|
||||||
|
.then((conf) => res.json(conf))
|
||||||
|
.catch((err) =>
|
||||||
|
res.status(503).send(err?.message ?? err ?? 'Unknown error'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch('/v1/device/host-config', async (req, res) => {
|
||||||
|
// Because v1 endpoints are legacy, and this endpoint might already be used
|
||||||
|
// by multiple users, adding too many throws might have unintended side effects.
|
||||||
|
// Thus we're simply logging invalid fields and allowing the request to continue.
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!req.body.network) {
|
||||||
|
log.warn("Key 'network' must exist in PATCH body");
|
||||||
|
// If network does not exist, skip all field validation checks below
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { proxy } = req.body.network;
|
||||||
|
|
||||||
|
// Validate proxy fields, if they exist
|
||||||
|
if (proxy && Object.keys(proxy).length) {
|
||||||
|
const blacklistedFields = Object.keys(proxy).filter((key) =>
|
||||||
|
disallowedHostConfigPatchFields.includes(key),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (blacklistedFields.length > 0) {
|
||||||
|
log.warn(`Invalid proxy field(s): ${blacklistedFields.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
proxy.type &&
|
||||||
|
!constants.validRedsocksProxyTypes.includes(proxy.type)
|
||||||
|
) {
|
||||||
|
log.warn(
|
||||||
|
`Invalid redsocks proxy type, must be one of ${constants.validRedsocksProxyTypes.join(
|
||||||
|
', ',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy.noProxy && !Array.isArray(proxy.noProxy)) {
|
||||||
|
log.warn('noProxy field must be an array of addresses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If hostname is an empty string, return first 7 digits of device uuid
|
||||||
|
if (req.body.network?.hostname === '') {
|
||||||
|
const uuid = await config.get('uuid');
|
||||||
|
req.body.network.hostname = uuid?.slice(0, 7);
|
||||||
|
}
|
||||||
|
const lockOverride = await config.get('lockOverride');
|
||||||
|
await hostConfig.patch(
|
||||||
|
req.body,
|
||||||
|
checkTruthy(req.body.force) || lockOverride,
|
||||||
|
);
|
||||||
|
res.status(200).send('OK');
|
||||||
|
} catch (err: any) {
|
||||||
|
// TODO: We should be able to throw err if it's UpdatesLockedError
|
||||||
|
// and the error middleware will handle it, but this doesn't work in
|
||||||
|
// the test environment. Fix this when fixing API tests.
|
||||||
|
if (err instanceof UpdatesLockedError) {
|
||||||
|
return res.status(423).send(err?.message ?? err);
|
||||||
|
}
|
||||||
|
res.status(503).send(err?.message ?? err ?? 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/v1/device', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const state = await deviceState.getLegacyState();
|
||||||
|
const stateToSend = _.pick(state.local, [
|
||||||
|
'api_port',
|
||||||
|
'ip_address',
|
||||||
|
'os_version',
|
||||||
|
'mac_address',
|
||||||
|
'supervisor_version',
|
||||||
|
'update_pending',
|
||||||
|
'update_failed',
|
||||||
|
'update_downloaded',
|
||||||
|
]) as Dictionary<unknown>;
|
||||||
|
if (state.local?.is_on__commit != null) {
|
||||||
|
stateToSend.commit = state.local.is_on__commit;
|
||||||
|
}
|
||||||
|
const service = _.toPairs(
|
||||||
|
_.toPairs(state.local?.apps)[0]?.[1]?.services,
|
||||||
|
)[0]?.[1];
|
||||||
|
|
||||||
|
if (service != null) {
|
||||||
|
stateToSend.status = service.status;
|
||||||
|
if (stateToSend.status === 'Running') {
|
||||||
|
stateToSend.status = 'Idle';
|
||||||
|
}
|
||||||
|
stateToSend.download_progress = service.download_progress;
|
||||||
|
}
|
||||||
|
res.json(stateToSend);
|
||||||
|
} catch (e: any) {
|
||||||
|
res.status(500).json({
|
||||||
|
Data: '',
|
||||||
|
Error: (e != null ? e.message : undefined) || e || 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
1007
src/device-api/v2.ts
1007
src/device-api/v2.ts
File diff suppressed because it is too large
Load Diff
@ -1,25 +1,20 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
import { stripIndent } from 'common-tags';
|
import { stripIndent } from 'common-tags';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as express from 'express';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import StrictEventEmitter from 'strict-event-emitter-types';
|
import StrictEventEmitter from 'strict-event-emitter-types';
|
||||||
import { isRight } from 'fp-ts/lib/Either';
|
import { isRight } from 'fp-ts/lib/Either';
|
||||||
import Reporter from 'io-ts-reporters';
|
import Reporter from 'io-ts-reporters';
|
||||||
|
|
||||||
import prettyMs = require('pretty-ms');
|
import prettyMs = require('pretty-ms');
|
||||||
|
|
||||||
import * as config from './config';
|
import * as config from './config';
|
||||||
import * as db from './db';
|
import * as db from './db';
|
||||||
import * as logger from './logger';
|
import * as logger from './logger';
|
||||||
|
|
||||||
import {
|
|
||||||
CompositionStepT,
|
|
||||||
CompositionStepAction,
|
|
||||||
} from './compose/composition-steps';
|
|
||||||
import { loadTargetFromFile } from './device-state/preload';
|
|
||||||
import * as globalEventBus from './event-bus';
|
import * as globalEventBus from './event-bus';
|
||||||
import * as hostConfig from './host-config';
|
import * as network from './network';
|
||||||
|
import * as deviceConfig from './device-config';
|
||||||
|
|
||||||
import constants = require('./lib/constants');
|
import constants = require('./lib/constants');
|
||||||
import * as dbus from './lib/dbus';
|
import * as dbus from './lib/dbus';
|
||||||
import {
|
import {
|
||||||
@ -28,14 +23,14 @@ import {
|
|||||||
UpdatesLockedError,
|
UpdatesLockedError,
|
||||||
} from './lib/errors';
|
} from './lib/errors';
|
||||||
import * as updateLock from './lib/update-lock';
|
import * as updateLock from './lib/update-lock';
|
||||||
import * as validation from './lib/validation';
|
import * as dbFormat from './device-state/db-format';
|
||||||
import * as network from './network';
|
import { getGlobalApiKey } from './device-api';
|
||||||
|
import * as sysInfo from './lib/system-info';
|
||||||
|
import { log } from './lib/supervisor-console';
|
||||||
|
import { loadTargetFromFile } from './device-state/preload';
|
||||||
import * as applicationManager from './compose/application-manager';
|
import * as applicationManager from './compose/application-manager';
|
||||||
import * as commitStore from './compose/commit';
|
import * as commitStore from './compose/commit';
|
||||||
import * as deviceConfig from './device-config';
|
|
||||||
import { ConfigStep } from './device-config';
|
|
||||||
import { log } from './lib/supervisor-console';
|
|
||||||
import {
|
import {
|
||||||
DeviceLegacyState,
|
DeviceLegacyState,
|
||||||
InstancedDeviceState,
|
InstancedDeviceState,
|
||||||
@ -44,11 +39,10 @@ import {
|
|||||||
DeviceReport,
|
DeviceReport,
|
||||||
AppState,
|
AppState,
|
||||||
} from './types';
|
} from './types';
|
||||||
import * as dbFormat from './device-state/db-format';
|
import type {
|
||||||
import * as apiKeys from './lib/api-keys';
|
CompositionStepT,
|
||||||
import * as sysInfo from './lib/system-info';
|
CompositionStepAction,
|
||||||
|
} from './compose/composition-steps';
|
||||||
const disallowedHostConfigPatchFields = ['local_ip', 'local_port'];
|
|
||||||
|
|
||||||
function parseTargetState(state: unknown): TargetState {
|
function parseTargetState(state: unknown): TargetState {
|
||||||
const res = TargetState.decode(state);
|
const res = TargetState.decode(state);
|
||||||
@ -61,151 +55,6 @@ function parseTargetState(state: unknown): TargetState {
|
|||||||
throw new TargetStateError(errors.join('\n'));
|
throw new TargetStateError(errors.join('\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (refactor): This shouldn't be here, and instead should be part of the other
|
|
||||||
// device api stuff in ./device-api
|
|
||||||
function createDeviceStateRouter() {
|
|
||||||
router = express.Router();
|
|
||||||
router.use(express.urlencoded({ limit: '10mb', extended: true }));
|
|
||||||
router.use(express.json({ limit: '10mb' }));
|
|
||||||
|
|
||||||
const rebootOrShutdown = async (
|
|
||||||
req: express.Request,
|
|
||||||
res: express.Response,
|
|
||||||
action: DeviceStateStepTarget,
|
|
||||||
) => {
|
|
||||||
const override = await config.get('lockOverride');
|
|
||||||
const force = validation.checkTruthy(req.body.force) || override;
|
|
||||||
try {
|
|
||||||
const response = await executeStepAction({ action }, { force });
|
|
||||||
res.status(202).json(response);
|
|
||||||
} catch (e: any) {
|
|
||||||
const status = e instanceof UpdatesLockedError ? 423 : 500;
|
|
||||||
res.status(status).json({
|
|
||||||
Data: '',
|
|
||||||
Error: (e != null ? e.message : undefined) || e || 'Unknown error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
router.post('/v1/reboot', (req, res) => rebootOrShutdown(req, res, 'reboot'));
|
|
||||||
router.post('/v1/shutdown', (req, res) =>
|
|
||||||
rebootOrShutdown(req, res, 'shutdown'),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get('/v1/device/host-config', (_req, res) =>
|
|
||||||
hostConfig
|
|
||||||
.get()
|
|
||||||
.then((conf) => res.json(conf))
|
|
||||||
.catch((err) =>
|
|
||||||
res.status(503).send(err?.message ?? err ?? 'Unknown error'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.patch('/v1/device/host-config', async (req, res) => {
|
|
||||||
// Because v1 endpoints are legacy, and this endpoint might already be used
|
|
||||||
// by multiple users, adding too many throws might have unintended side effects.
|
|
||||||
// Thus we're simply logging invalid fields and allowing the request to continue.
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!req.body.network) {
|
|
||||||
log.warn("Key 'network' must exist in PATCH body");
|
|
||||||
// If network does not exist, skip all field validation checks below
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { proxy } = req.body.network;
|
|
||||||
|
|
||||||
// Validate proxy fields, if they exist
|
|
||||||
if (proxy && Object.keys(proxy).length) {
|
|
||||||
const blacklistedFields = Object.keys(proxy).filter((key) =>
|
|
||||||
disallowedHostConfigPatchFields.includes(key),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (blacklistedFields.length > 0) {
|
|
||||||
log.warn(`Invalid proxy field(s): ${blacklistedFields.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
proxy.type &&
|
|
||||||
!constants.validRedsocksProxyTypes.includes(proxy.type)
|
|
||||||
) {
|
|
||||||
log.warn(
|
|
||||||
`Invalid redsocks proxy type, must be one of ${constants.validRedsocksProxyTypes.join(
|
|
||||||
', ',
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proxy.noProxy && !Array.isArray(proxy.noProxy)) {
|
|
||||||
log.warn('noProxy field must be an array of addresses');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If hostname is an empty string, return first 7 digits of device uuid
|
|
||||||
if (req.body.network?.hostname === '') {
|
|
||||||
const uuid = await config.get('uuid');
|
|
||||||
req.body.network.hostname = uuid?.slice(0, 7);
|
|
||||||
}
|
|
||||||
const lockOverride = await config.get('lockOverride');
|
|
||||||
await hostConfig.patch(
|
|
||||||
req.body,
|
|
||||||
validation.checkTruthy(req.body.force) || lockOverride,
|
|
||||||
);
|
|
||||||
res.status(200).send('OK');
|
|
||||||
} catch (err: any) {
|
|
||||||
// TODO: We should be able to throw err if it's UpdatesLockedError
|
|
||||||
// and the error middleware will handle it, but this doesn't work in
|
|
||||||
// the test environment. Fix this when fixing API tests.
|
|
||||||
if (err instanceof UpdatesLockedError) {
|
|
||||||
return res.status(423).send(err?.message ?? err);
|
|
||||||
}
|
|
||||||
res.status(503).send(err?.message ?? err ?? 'Unknown error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/v1/device', async (_req, res) => {
|
|
||||||
try {
|
|
||||||
const state = await getLegacyState();
|
|
||||||
const stateToSend = _.pick(state.local, [
|
|
||||||
'api_port',
|
|
||||||
'ip_address',
|
|
||||||
'os_version',
|
|
||||||
'mac_address',
|
|
||||||
'supervisor_version',
|
|
||||||
'update_pending',
|
|
||||||
'update_failed',
|
|
||||||
'update_downloaded',
|
|
||||||
]) as Dictionary<unknown>;
|
|
||||||
if (state.local?.is_on__commit != null) {
|
|
||||||
stateToSend.commit = state.local.is_on__commit;
|
|
||||||
}
|
|
||||||
const service = _.toPairs(
|
|
||||||
_.toPairs(state.local?.apps)[0]?.[1]?.services,
|
|
||||||
)[0]?.[1];
|
|
||||||
|
|
||||||
if (service != null) {
|
|
||||||
stateToSend.status = service.status;
|
|
||||||
if (stateToSend.status === 'Running') {
|
|
||||||
stateToSend.status = 'Idle';
|
|
||||||
}
|
|
||||||
stateToSend.download_progress = service.download_progress;
|
|
||||||
}
|
|
||||||
res.json(stateToSend);
|
|
||||||
} catch (e: any) {
|
|
||||||
res.status(500).json({
|
|
||||||
Data: '',
|
|
||||||
Error: (e != null ? e.message : undefined) || e || 'Unknown error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.use(applicationManager.router);
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeviceStateEvents {
|
interface DeviceStateEvents {
|
||||||
error: Error;
|
error: Error;
|
||||||
change: void;
|
change: void;
|
||||||
@ -236,7 +85,7 @@ export const removeListener: typeof events['removeListener'] =
|
|||||||
export const removeAllListeners: typeof events['removeAllListeners'] =
|
export const removeAllListeners: typeof events['removeAllListeners'] =
|
||||||
events.removeAllListeners.bind(events);
|
events.removeAllListeners.bind(events);
|
||||||
|
|
||||||
type DeviceStateStepTarget = 'reboot' | 'shutdown' | 'noop';
|
export type DeviceStateStepTarget = 'reboot' | 'shutdown' | 'noop';
|
||||||
|
|
||||||
type PossibleStepTargets = CompositionStepAction | DeviceStateStepTarget;
|
type PossibleStepTargets = CompositionStepAction | DeviceStateStepTarget;
|
||||||
type DeviceStateStep<T extends PossibleStepTargets> =
|
type DeviceStateStep<T extends PossibleStepTargets> =
|
||||||
@ -246,7 +95,7 @@ type DeviceStateStep<T extends PossibleStepTargets> =
|
|||||||
| { action: 'shutdown' }
|
| { action: 'shutdown' }
|
||||||
| { action: 'noop' }
|
| { action: 'noop' }
|
||||||
| CompositionStepT<T extends CompositionStepAction ? T : never>
|
| CompositionStepT<T extends CompositionStepAction ? T : never>
|
||||||
| ConfigStep;
|
| deviceConfig.ConfigStep;
|
||||||
|
|
||||||
let currentVolatile: DeviceReport = {};
|
let currentVolatile: DeviceReport = {};
|
||||||
const writeLock = updateLock.writeLock;
|
const writeLock = updateLock.writeLock;
|
||||||
@ -266,8 +115,6 @@ let applyInProgress = false;
|
|||||||
export let connected: boolean;
|
export let connected: boolean;
|
||||||
export let lastSuccessfulUpdate: number | null = null;
|
export let lastSuccessfulUpdate: number | null = null;
|
||||||
|
|
||||||
export let router: express.Router;
|
|
||||||
|
|
||||||
events.on('error', (err) => log.error('deviceState error: ', err));
|
events.on('error', (err) => log.error('deviceState error: ', err));
|
||||||
events.on('apply-target-state-end', function (err) {
|
events.on('apply-target-state-end', function (err) {
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
@ -288,7 +135,6 @@ export const initialized = _.once(async () => {
|
|||||||
await applicationManager.initialized();
|
await applicationManager.initialized();
|
||||||
|
|
||||||
applicationManager.on('change', (d) => reportCurrentState(d));
|
applicationManager.on('change', (d) => reportCurrentState(d));
|
||||||
createDeviceStateRouter();
|
|
||||||
|
|
||||||
config.on('change', (changedConfig) => {
|
config.on('change', (changedConfig) => {
|
||||||
if (changedConfig.loggingEnabled != null) {
|
if (changedConfig.loggingEnabled != null) {
|
||||||
@ -373,7 +219,6 @@ async function saveInitialConfig() {
|
|||||||
|
|
||||||
export async function loadInitialState() {
|
export async function loadInitialState() {
|
||||||
await applicationManager.initialized();
|
await applicationManager.initialized();
|
||||||
await apiKeys.initialized();
|
|
||||||
|
|
||||||
const conf = await config.getMany([
|
const conf = await config.getMany([
|
||||||
'initialConfigSaved',
|
'initialConfigSaved',
|
||||||
@ -399,9 +244,10 @@ export async function loadInitialState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info('Reporting initial state, supervisor version and API info');
|
log.info('Reporting initial state, supervisor version and API info');
|
||||||
|
const globalApiKey = await getGlobalApiKey();
|
||||||
reportCurrentState({
|
reportCurrentState({
|
||||||
api_port: conf.listenPort,
|
api_port: conf.listenPort,
|
||||||
api_secret: apiKeys.cloudApiKey,
|
api_secret: globalApiKey,
|
||||||
os_version: conf.osVersion,
|
os_version: conf.osVersion,
|
||||||
os_variant: conf.osVariant,
|
os_variant: conf.osVariant,
|
||||||
mac_address: conf.macAddress,
|
mac_address: conf.macAddress,
|
||||||
@ -715,7 +561,7 @@ export async function executeStepAction<T extends PossibleStepTargets>(
|
|||||||
}: { force?: boolean; initial?: boolean; skipLock?: boolean },
|
}: { force?: boolean; initial?: boolean; skipLock?: boolean },
|
||||||
) {
|
) {
|
||||||
if (deviceConfig.isValidAction(step.action)) {
|
if (deviceConfig.isValidAction(step.action)) {
|
||||||
await deviceConfig.executeStepAction(step as ConfigStep, {
|
await deviceConfig.executeStepAction(step as deviceConfig.ConfigStep, {
|
||||||
initial,
|
initial,
|
||||||
});
|
});
|
||||||
} else if (_.includes(applicationManager.validActions, step.action)) {
|
} else if (_.includes(applicationManager.validActions, step.action)) {
|
||||||
|
@ -1,15 +1,27 @@
|
|||||||
import { getBus, Error as DBusError } from 'dbus';
|
import { getBus, Error as DBusError } from 'dbus';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { TypedError } from 'typed-error';
|
import { TypedError } from 'typed-error';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import log from './supervisor-console';
|
import log from './supervisor-console';
|
||||||
|
import DBus = require('dbus');
|
||||||
|
|
||||||
export class DbusError extends TypedError {}
|
export class DbusError extends TypedError {}
|
||||||
|
|
||||||
const bus = getBus('system');
|
let bus: DBus.DBusConnection;
|
||||||
const getInterfaceAsync = promisify(bus.getInterface.bind(bus));
|
let getInterfaceAsync: <T = DBus.AnyInterfaceMethod>(
|
||||||
|
serviceName: string,
|
||||||
|
objectPath: string,
|
||||||
|
ifaceName: string,
|
||||||
|
) => Promise<DBus.DBusInterface<T>>;
|
||||||
|
|
||||||
|
export const initialized = _.once(async () => {
|
||||||
|
bus = getBus('system');
|
||||||
|
getInterfaceAsync = promisify(bus.getInterface.bind(bus));
|
||||||
|
});
|
||||||
|
|
||||||
async function getSystemdInterface() {
|
async function getSystemdInterface() {
|
||||||
|
await initialized();
|
||||||
try {
|
try {
|
||||||
return await getInterfaceAsync(
|
return await getInterfaceAsync(
|
||||||
'org.freedesktop.systemd1',
|
'org.freedesktop.systemd1',
|
||||||
@ -21,7 +33,8 @@ async function getSystemdInterface() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLoginManagerInterface() {
|
async function getLoginManagerInterface() {
|
||||||
|
await initialized();
|
||||||
try {
|
try {
|
||||||
return await getInterfaceAsync(
|
return await getInterfaceAsync(
|
||||||
'org.freedesktop.login1',
|
'org.freedesktop.login1',
|
||||||
|
@ -87,10 +87,8 @@ const formatCurrentAsState = (device) => ({
|
|||||||
config: device.config,
|
config: device.config,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createProxyvisorRouter = function (proxyvisor) {
|
const createProxyvisorRouter = function (pv) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(express.urlencoded({ limit: '10mb', extended: true }));
|
|
||||||
router.use(express.json({ limit: '10mb' }));
|
|
||||||
router.get('/v1/devices', async (_req, res) => {
|
router.get('/v1/devices', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const fields = await db.models('dependentDevice').select();
|
const fields = await db.models('dependentDevice').select();
|
||||||
@ -315,7 +313,7 @@ const createProxyvisorRouter = function (proxyvisor) {
|
|||||||
await fs.lstat(dest);
|
await fs.lstat(dest);
|
||||||
} catch {
|
} catch {
|
||||||
await Promise.using(
|
await Promise.using(
|
||||||
proxyvisor.docker.imageRootDirMounted(app.image),
|
pv.docker.imageRootDirMounted(app.image),
|
||||||
(rootDir) => getTarArchive(rootDir + '/assets', dest),
|
(rootDir) => getTarArchive(rootDir + '/assets', dest),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -346,7 +344,7 @@ const createProxyvisorRouter = function (proxyvisor) {
|
|||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Proxyvisor {
|
class Proxyvisor {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.executeStepAction = this.executeStepAction.bind(this);
|
this.executeStepAction = this.executeStepAction.bind(this);
|
||||||
this.getCurrentStates = this.getCurrentStates.bind(this);
|
this.getCurrentStates = this.getCurrentStates.bind(this);
|
||||||
@ -1003,3 +1001,6 @@ export class Proxyvisor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proxyvisor = new Proxyvisor();
|
||||||
|
export default proxyvisor;
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
import { NextFunction, Request, Response } from 'express';
|
|
||||||
import * as express from 'express';
|
|
||||||
import { Server } from 'http';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as morgan from 'morgan';
|
|
||||||
|
|
||||||
import * as eventTracker from './event-tracker';
|
|
||||||
import blink = require('./lib/blink');
|
|
||||||
|
|
||||||
import log from './lib/supervisor-console';
|
|
||||||
import * as apiKeys from './lib/api-keys';
|
|
||||||
import * as deviceState from './device-state';
|
|
||||||
import { UpdatesLockedError } from './lib/errors';
|
|
||||||
|
|
||||||
interface SupervisorAPIConstructOpts {
|
|
||||||
routers: express.Router[];
|
|
||||||
healthchecks: Array<() => Promise<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SupervisorAPIStopOpts {
|
|
||||||
errored: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SupervisorAPI {
|
|
||||||
private routers: express.Router[];
|
|
||||||
private healthchecks: Array<() => Promise<boolean>>;
|
|
||||||
|
|
||||||
private api = express();
|
|
||||||
private server: Server | null = null;
|
|
||||||
|
|
||||||
public constructor({ routers, healthchecks }: SupervisorAPIConstructOpts) {
|
|
||||||
this.routers = routers;
|
|
||||||
this.healthchecks = healthchecks;
|
|
||||||
|
|
||||||
this.api.disable('x-powered-by');
|
|
||||||
this.api.use(
|
|
||||||
morgan(
|
|
||||||
(tokens, req, res) =>
|
|
||||||
[
|
|
||||||
tokens.method(req, res),
|
|
||||||
req.path,
|
|
||||||
tokens.status(req, res),
|
|
||||||
'-',
|
|
||||||
tokens['response-time'](req, res),
|
|
||||||
'ms',
|
|
||||||
].join(' '),
|
|
||||||
{
|
|
||||||
stream: { write: (d) => log.api(d.toString().trimRight()) },
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.api.get('/v1/healthy', async (_req, res) => {
|
|
||||||
try {
|
|
||||||
const healths = await Promise.all(this.healthchecks.map((fn) => fn()));
|
|
||||||
if (!_.every(healths)) {
|
|
||||||
log.error('Healthcheck failed');
|
|
||||||
return res.status(500).send('Unhealthy');
|
|
||||||
}
|
|
||||||
return res.sendStatus(200);
|
|
||||||
} catch {
|
|
||||||
log.error('Healthcheck failed');
|
|
||||||
return res.status(500).send('Unhealthy');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.get('/ping', (_req, res) => res.send('OK'));
|
|
||||||
|
|
||||||
this.api.use(apiKeys.authMiddleware);
|
|
||||||
|
|
||||||
this.api.post('/v1/blink', (_req, res) => {
|
|
||||||
eventTracker.track('Device blink');
|
|
||||||
blink.pattern.start();
|
|
||||||
setTimeout(blink.pattern.stop, 15000);
|
|
||||||
return res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expires the supervisor's API key and generates a new one.
|
|
||||||
// It also communicates the new key to the balena API.
|
|
||||||
this.api.post(
|
|
||||||
'/v1/regenerate-api-key',
|
|
||||||
async (req: apiKeys.AuthorizedRequest, res) => {
|
|
||||||
await deviceState.initialized();
|
|
||||||
await apiKeys.initialized();
|
|
||||||
|
|
||||||
// check if we're updating the cloud API key
|
|
||||||
const updateCloudKey = req.auth.apiKey === apiKeys.cloudApiKey;
|
|
||||||
|
|
||||||
// regenerate the key...
|
|
||||||
const newKey = await apiKeys.refreshKey(req.auth.apiKey);
|
|
||||||
|
|
||||||
// if we need to update the cloud API with our new key
|
|
||||||
if (updateCloudKey) {
|
|
||||||
// report the new key to the cloud API
|
|
||||||
deviceState.reportCurrentState({
|
|
||||||
api_secret: apiKeys.cloudApiKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the value of the new key to the caller
|
|
||||||
res.status(200).send(newKey);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// And assign all external routers
|
|
||||||
for (const router of this.routers) {
|
|
||||||
this.api.use(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error handling.
|
|
||||||
const messageFromError = (err?: Error | string | null): string => {
|
|
||||||
let message = 'Unknown error';
|
|
||||||
if (err != null) {
|
|
||||||
if (_.isError(err) && err.message != null) {
|
|
||||||
message = err.message;
|
|
||||||
} else {
|
|
||||||
message = err as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.api.use(
|
|
||||||
(err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
||||||
if (res.headersSent) {
|
|
||||||
// Error happens while we are writing the response - default handler closes the connection.
|
|
||||||
next(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return 423 Locked when locks as set
|
|
||||||
const code = err instanceof UpdatesLockedError ? 423 : 503;
|
|
||||||
if (code !== 423) {
|
|
||||||
log.error(`Error on ${req.method} ${req.path}: `, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(code).send({
|
|
||||||
status: 'failed',
|
|
||||||
message: messageFromError(err),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async listen(port: number, apiTimeout: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.server = this.api.listen(port, () => {
|
|
||||||
log.info(`Supervisor API successfully started on port ${port}`);
|
|
||||||
if (this.server) {
|
|
||||||
this.server.timeout = apiTimeout;
|
|
||||||
}
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop(options?: SupervisorAPIStopOpts): Promise<void> {
|
|
||||||
if (this.server != null) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.server?.close((err: Error) => {
|
|
||||||
if (err) {
|
|
||||||
log.error('Failed to stop Supervisor API');
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
options?.errored
|
|
||||||
? log.error('Stopped Supervisor API')
|
|
||||||
: log.info('Stopped Supervisor API');
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SupervisorAPI;
|
|
@ -2,16 +2,19 @@ import * as apiBinder from './api-binder';
|
|||||||
import * as db from './db';
|
import * as db from './db';
|
||||||
import * as config from './config';
|
import * as config from './config';
|
||||||
import * as deviceState from './device-state';
|
import * as deviceState from './device-state';
|
||||||
|
import * as logger from './logger';
|
||||||
|
import SupervisorAPI from './device-api';
|
||||||
|
import * as v1 from './device-api/v1';
|
||||||
|
import * as v2 from './device-api/v2';
|
||||||
|
import logMonitor from './logging/monitor';
|
||||||
|
|
||||||
import { intialiseContractRequirements } from './lib/contracts';
|
import { intialiseContractRequirements } from './lib/contracts';
|
||||||
import { normaliseLegacyDatabase } from './lib/legacy';
|
import { normaliseLegacyDatabase } from './lib/legacy';
|
||||||
import * as osRelease from './lib/os-release';
|
import * as osRelease from './lib/os-release';
|
||||||
import * as logger from './logger';
|
|
||||||
import SupervisorAPI from './supervisor-api';
|
|
||||||
import log from './lib/supervisor-console';
|
import log from './lib/supervisor-console';
|
||||||
import version = require('./lib/supervisor-version');
|
import version = require('./lib/supervisor-version');
|
||||||
import * as avahi from './lib/avahi';
|
import * as avahi from './lib/avahi';
|
||||||
import * as firewall from './lib/firewall';
|
import * as firewall from './lib/firewall';
|
||||||
import logMonitor from './logging/monitor';
|
|
||||||
|
|
||||||
const startupConfigFields: config.ConfigKey[] = [
|
const startupConfigFields: config.ConfigKey[] = [
|
||||||
'uuid',
|
'uuid',
|
||||||
@ -59,14 +62,13 @@ export class Supervisor {
|
|||||||
await normaliseLegacyDatabase();
|
await normaliseLegacyDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the state engine, the device API and API binder
|
// Start the state engine, the device API and API binder in parallel
|
||||||
// in parallel
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deviceState.loadInitialState(),
|
deviceState.loadInitialState(),
|
||||||
(() => {
|
(() => {
|
||||||
log.info('Starting API server');
|
log.info('Starting API server');
|
||||||
this.api = new SupervisorAPI({
|
this.api = new SupervisorAPI({
|
||||||
routers: [apiBinder.router, deviceState.router],
|
routers: [v1.router, v2.router],
|
||||||
healthchecks: [apiBinder.healthcheck, deviceState.healthcheck],
|
healthchecks: [apiBinder.healthcheck, deviceState.healthcheck],
|
||||||
});
|
});
|
||||||
this.api.listen(conf.listenPort, conf.apiTimeout);
|
this.api.listen(conf.listenPort, conf.apiTimeout);
|
||||||
|
@ -3,7 +3,7 @@ import * as _ from 'lodash';
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import Service from '~/src/compose/service';
|
import Service from '~/src/compose/service';
|
||||||
import * as apiKeys from '~/lib/api-keys';
|
import * as deviceApi from '~/src/device-api';
|
||||||
|
|
||||||
describe('compose/service: integration tests', () => {
|
describe('compose/service: integration tests', () => {
|
||||||
describe('Feature labels', () => {
|
describe('Feature labels', () => {
|
||||||
@ -43,7 +43,7 @@ describe('compose/service: integration tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets BALENA_API_KEY env var to the scoped API key value', async () => {
|
it('sets BALENA_API_KEY env var to the scoped API key value', async () => {
|
||||||
const mykey = await apiKeys.generateScopedKey(123456, 'foobar');
|
const mykey = await deviceApi.generateScopedKey(123456, 'foobar');
|
||||||
|
|
||||||
const service = await Service.fromComposeObject(
|
const service = await Service.fromComposeObject(
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@ module.exports = {
|
|||||||
'tsconfig-paths/register',
|
'tsconfig-paths/register',
|
||||||
'test/lib/chai.ts',
|
'test/lib/chai.ts',
|
||||||
'test/legacy/fixtures.ts',
|
'test/legacy/fixtures.ts',
|
||||||
|
'test/lib/legacy-mocha-hooks.ts'
|
||||||
],
|
],
|
||||||
spec: ['test/legacy/**/*.spec.ts'],
|
spec: ['test/legacy/**/*.spec.ts'],
|
||||||
timeout: '30000',
|
timeout: '30000',
|
||||||
|
@ -6,10 +6,9 @@ import mockedAPI = require('~/test-lib/mocked-device-api');
|
|||||||
import * as apiBinder from '~/src/api-binder';
|
import * as apiBinder from '~/src/api-binder';
|
||||||
import * as deviceState from '~/src/device-state';
|
import * as deviceState from '~/src/device-state';
|
||||||
import Log from '~/lib/supervisor-console';
|
import Log from '~/lib/supervisor-console';
|
||||||
import SupervisorAPI from '~/src/supervisor-api';
|
|
||||||
import * as apiKeys from '~/lib/api-keys';
|
|
||||||
import * as db from '~/src/db';
|
import * as db from '~/src/db';
|
||||||
import { cloudApiKey } from '~/lib/api-keys';
|
import SupervisorAPI from '~/src/device-api';
|
||||||
|
import * as deviceApi from '~/src/device-api';
|
||||||
|
|
||||||
const mockedOptions = {
|
const mockedOptions = {
|
||||||
listenPort: 54321,
|
listenPort: 54321,
|
||||||
@ -30,10 +29,6 @@ describe('SupervisorAPI', () => {
|
|||||||
|
|
||||||
// Start test API
|
// Start test API
|
||||||
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
||||||
|
|
||||||
// Create a scoped key
|
|
||||||
await apiKeys.initialized();
|
|
||||||
await apiKeys.generateCloudKey();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
@ -56,7 +51,7 @@ describe('SupervisorAPI', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/ping')
|
.get('/ping')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -64,7 +59,7 @@ describe('SupervisorAPI', () => {
|
|||||||
describe('API Key Scope', () => {
|
describe('API Key Scope', () => {
|
||||||
it('should generate a key which is scoped for a single application', async () => {
|
it('should generate a key which is scoped for a single application', async () => {
|
||||||
// single app scoped key...
|
// single app scoped key...
|
||||||
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
|
const appScopedKey = await deviceApi.generateScopedKey(1, 'main');
|
||||||
|
|
||||||
await request
|
await request
|
||||||
.get('/v2/applications/1/state')
|
.get('/v2/applications/1/state')
|
||||||
@ -74,7 +69,7 @@ describe('SupervisorAPI', () => {
|
|||||||
});
|
});
|
||||||
it('should generate a key which is scoped for multiple applications', async () => {
|
it('should generate a key which is scoped for multiple applications', async () => {
|
||||||
// multi-app scoped key...
|
// multi-app scoped key...
|
||||||
const multiAppScopedKey = await apiKeys.generateScopedKey(1, 'other', {
|
const multiAppScopedKey = await deviceApi.generateScopedKey(1, 'other', {
|
||||||
scopes: [1, 2].map((appId) => {
|
scopes: [1, 2].map((appId) => {
|
||||||
return { type: 'app', appId };
|
return { type: 'app', appId };
|
||||||
}),
|
}),
|
||||||
@ -96,17 +91,19 @@ describe('SupervisorAPI', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v2/applications/1/state')
|
.get('/v2/applications/1/state')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
await request
|
await request
|
||||||
.get('/v2/applications/2/state')
|
.get('/v2/applications/2/state')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
it('should have a cached lookup of the key scopes to save DB loading', async () => {
|
it('should have a cached lookup of the key scopes to save DB loading', async () => {
|
||||||
const scopes = await apiKeys.getScopesForKey(apiKeys.cloudApiKey);
|
const scopes = await deviceApi.getScopesForKey(
|
||||||
|
await deviceApi.getGlobalApiKey(),
|
||||||
|
);
|
||||||
|
|
||||||
const key = 'not-a-normal-key';
|
const key = 'not-a-normal-key';
|
||||||
await db.initialized();
|
await db.initialized();
|
||||||
@ -116,26 +113,30 @@ describe('SupervisorAPI', () => {
|
|||||||
key,
|
key,
|
||||||
})
|
})
|
||||||
.where({
|
.where({
|
||||||
key: apiKeys.cloudApiKey,
|
key: await deviceApi.getGlobalApiKey(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// the key we had is now gone, but the cache should return values
|
// the key we had is now gone, but the cache should return values
|
||||||
const cachedScopes = await apiKeys.getScopesForKey(apiKeys.cloudApiKey);
|
const cachedScopes = await deviceApi.getScopesForKey(
|
||||||
|
await deviceApi.getGlobalApiKey(),
|
||||||
|
);
|
||||||
expect(cachedScopes).to.deep.equal(scopes);
|
expect(cachedScopes).to.deep.equal(scopes);
|
||||||
|
|
||||||
// this should bust the cache...
|
// this should bust the cache...
|
||||||
await apiKeys.generateCloudKey(true);
|
await deviceApi.refreshKey(await deviceApi.getGlobalApiKey());
|
||||||
|
|
||||||
// the key we changed should be gone now, and the new key should have the cloud scopes
|
// the key we changed should be gone now, and the new key should have the cloud scopes
|
||||||
const missingScopes = await apiKeys.getScopesForKey(key);
|
const missingScopes = await deviceApi.getScopesForKey(key);
|
||||||
const freshScopes = await apiKeys.getScopesForKey(apiKeys.cloudApiKey);
|
const freshScopes = await deviceApi.getScopesForKey(
|
||||||
|
await deviceApi.getGlobalApiKey(),
|
||||||
|
);
|
||||||
|
|
||||||
expect(missingScopes).to.be.null;
|
expect(missingScopes).to.be.null;
|
||||||
expect(freshScopes).to.deep.equal(scopes);
|
expect(freshScopes).to.deep.equal(scopes);
|
||||||
});
|
});
|
||||||
it('should regenerate a key and invalidate the old one', async () => {
|
it('should regenerate a key and invalidate the old one', async () => {
|
||||||
// single app scoped key...
|
// single app scoped key...
|
||||||
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
|
const appScopedKey = await deviceApi.generateScopedKey(1, 'main');
|
||||||
|
|
||||||
await request
|
await request
|
||||||
.get('/v2/applications/1/state')
|
.get('/v2/applications/1/state')
|
||||||
@ -143,7 +144,7 @@ describe('SupervisorAPI', () => {
|
|||||||
.set('Authorization', `Bearer ${appScopedKey}`)
|
.set('Authorization', `Bearer ${appScopedKey}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const newScopedKey = await apiKeys.refreshKey(appScopedKey);
|
const newScopedKey = await deviceApi.refreshKey(appScopedKey);
|
||||||
|
|
||||||
await request
|
await request
|
||||||
.get('/v2/applications/1/state')
|
.get('/v2/applications/1/state')
|
||||||
@ -225,20 +226,22 @@ describe('SupervisorAPI', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('finds apiKey from query', async () => {
|
it('finds apiKey from query', async () => {
|
||||||
return request.post(`/v1/blink?apikey=${cloudApiKey}`).expect(200);
|
return request
|
||||||
|
.post(`/v1/blink?apikey=${await deviceApi.getGlobalApiKey()}`)
|
||||||
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
|
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
|
||||||
return request
|
return request
|
||||||
.post('/v1/blink')
|
.post('/v1/blink')
|
||||||
.set('Authorization', `ApiKey ${cloudApiKey}`)
|
.set('Authorization', `ApiKey ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
|
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
|
||||||
return request
|
return request
|
||||||
.post('/v1/blink')
|
.post('/v1/blink')
|
||||||
.set('Authorization', `Bearer ${cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -256,7 +259,10 @@ describe('SupervisorAPI', () => {
|
|||||||
for (const scheme of randomCases) {
|
for (const scheme of randomCases) {
|
||||||
return request
|
return request
|
||||||
.post('/v1/blink')
|
.post('/v1/blink')
|
||||||
.set('Authorization', `${scheme} ${cloudApiKey}`)
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
`${scheme} ${await deviceApi.getGlobalApiKey()}`,
|
||||||
|
)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -20,17 +20,18 @@ import mockedAPI = require('~/test-lib/mocked-device-api');
|
|||||||
import sampleResponses = require('~/test-data/device-api-responses.json');
|
import sampleResponses = require('~/test-data/device-api-responses.json');
|
||||||
import * as config from '~/src/config';
|
import * as config from '~/src/config';
|
||||||
import * as logger from '~/src/logger';
|
import * as logger from '~/src/logger';
|
||||||
import SupervisorAPI from '~/src/supervisor-api';
|
import SupervisorAPI from '~/src/device-api';
|
||||||
|
import * as deviceApi from '~/src/device-api';
|
||||||
import * as apiBinder from '~/src/api-binder';
|
import * as apiBinder from '~/src/api-binder';
|
||||||
import * as deviceState from '~/src/device-state';
|
import * as deviceState from '~/src/device-state';
|
||||||
import * as apiKeys from '~/lib/api-keys';
|
import * as apiKeys from '~/src/device-api/api-keys';
|
||||||
import * as dbus from '~/lib/dbus';
|
import * as dbus from '~/lib/dbus';
|
||||||
import * as updateLock from '~/lib/update-lock';
|
import * as updateLock from '~/lib/update-lock';
|
||||||
import * as TargetState from '~/src/device-state/target-state';
|
import * as TargetState from '~/src/device-state/target-state';
|
||||||
import * as targetStateCache from '~/src/device-state/target-state-cache';
|
import * as targetStateCache from '~/src/device-state/target-state-cache';
|
||||||
import blink = require('~/lib/blink');
|
import blink = require('~/lib/blink');
|
||||||
import constants = require('~/lib/constants');
|
import constants = require('~/lib/constants');
|
||||||
import * as deviceAPI from '~/src/device-api/common';
|
import * as deviceAPIActions from '~/src/device-api/common';
|
||||||
import { UpdatesLockedError } from '~/lib/errors';
|
import { UpdatesLockedError } from '~/lib/errors';
|
||||||
import { SchemaTypeKey } from '~/src/config/schema-type';
|
import { SchemaTypeKey } from '~/src/config/schema-type';
|
||||||
import log from '~/lib/supervisor-console';
|
import log from '~/lib/supervisor-console';
|
||||||
@ -106,10 +107,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
// Mock target state cache
|
// Mock target state cache
|
||||||
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
|
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
|
||||||
|
|
||||||
// Create a scoped key
|
|
||||||
await apiKeys.initialized();
|
|
||||||
await apiKeys.generateCloudKey();
|
|
||||||
|
|
||||||
// Stub logs for all API methods
|
// Stub logs for all API methods
|
||||||
loggerStub = stub(logger, 'attach');
|
loggerStub = stub(logger, 'attach');
|
||||||
loggerStub.resolves();
|
loggerStub.resolves();
|
||||||
@ -141,7 +138,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.post('/v1/restart')
|
.post('/v1/restart')
|
||||||
.send({ appId: 2 })
|
.send({ appId: 2 })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/restart'].statusCode)
|
.expect(sampleResponses.V1.POST['/restart'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(
|
expect(response.body).to.deep.equal(
|
||||||
@ -170,7 +167,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.post('/v1/restart')
|
.post('/v1/restart')
|
||||||
.send({ thing: '' })
|
.send({ thing: '' })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/restart [Invalid Body]'].statusCode)
|
.expect(sampleResponses.V1.POST['/restart [Invalid Body]'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(
|
expect(response.body).to.deep.equal(
|
||||||
@ -190,7 +187,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/healthy')
|
.get('/v1/healthy')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.GET['/healthy'].statusCode)
|
.expect(sampleResponses.V1.GET['/healthy'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(
|
expect(response.body).to.deep.equal(
|
||||||
@ -223,7 +220,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/apps/2')
|
.get('/v1/apps/2')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(
|
.expect(
|
||||||
sampleResponses.V1.GET['/apps/2 [Multiple containers running]']
|
sampleResponses.V1.GET['/apps/2 [Multiple containers running]']
|
||||||
.statusCode,
|
.statusCode,
|
||||||
@ -249,7 +246,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/apps/2')
|
.get('/v1/apps/2')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
|
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -271,7 +268,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/apps/2/stop')
|
.post('/v1/apps/2/stop')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(
|
.expect(
|
||||||
sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]']
|
sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]']
|
||||||
.statusCode,
|
.statusCode,
|
||||||
@ -297,7 +294,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/apps/2/stop')
|
.post('/v1/apps/2/stop')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.GET['/apps/2/stop'].statusCode)
|
.expect(sampleResponses.V1.GET['/apps/2/stop'].statusCode)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -319,7 +316,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/apps/2/start')
|
.post('/v1/apps/2/start')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -356,7 +353,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/apps/2/start')
|
.post('/v1/apps/2/start')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -372,7 +369,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
const response = await request
|
const response = await request
|
||||||
.get('/v1/device')
|
.get('/v1/device')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body).to.have.property('mac_address').that.is.not.empty;
|
expect(response.body).to.have.property('mac_address').that.is.not.empty;
|
||||||
@ -412,7 +409,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
const response = await request
|
const response = await request
|
||||||
.post('/v1/reboot')
|
.post('/v1/reboot')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(202);
|
.expect(202);
|
||||||
|
|
||||||
expect(response.body).to.have.property('Data').that.is.not.empty;
|
expect(response.body).to.have.property('Data').that.is.not.empty;
|
||||||
@ -447,7 +444,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
const response = await request
|
const response = await request
|
||||||
.post('/v1/reboot')
|
.post('/v1/reboot')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(423);
|
.expect(423);
|
||||||
|
|
||||||
expect(updateLock.lock).to.be.calledOnce;
|
expect(updateLock.lock).to.be.calledOnce;
|
||||||
@ -486,7 +483,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.post('/v1/reboot')
|
.post('/v1/reboot')
|
||||||
.send({ force: true })
|
.send({ force: true })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(202);
|
.expect(202);
|
||||||
|
|
||||||
expect(updateLock.lock).to.be.calledOnce;
|
expect(updateLock.lock).to.be.calledOnce;
|
||||||
@ -528,7 +525,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
const response = await request
|
const response = await request
|
||||||
.post('/v1/shutdown')
|
.post('/v1/shutdown')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(202);
|
.expect(202);
|
||||||
|
|
||||||
expect(response.body).to.have.property('Data').that.is.not.empty;
|
expect(response.body).to.have.property('Data').that.is.not.empty;
|
||||||
@ -571,7 +568,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
const response = await request
|
const response = await request
|
||||||
.post('/v1/shutdown')
|
.post('/v1/shutdown')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(202);
|
.expect(202);
|
||||||
|
|
||||||
expect(lockSpy.callCount).to.equal(1);
|
expect(lockSpy.callCount).to.equal(1);
|
||||||
@ -612,7 +609,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
const response = await request
|
const response = await request
|
||||||
.post('/v1/shutdown')
|
.post('/v1/shutdown')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(423);
|
.expect(423);
|
||||||
|
|
||||||
expect(updateLock.lock).to.be.calledOnce;
|
expect(updateLock.lock).to.be.calledOnce;
|
||||||
@ -651,7 +648,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.post('/v1/shutdown')
|
.post('/v1/shutdown')
|
||||||
.send({ force: true })
|
.send({ force: true })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(202);
|
.expect(202);
|
||||||
|
|
||||||
expect(updateLock.lock).to.be.calledOnce;
|
expect(updateLock.lock).to.be.calledOnce;
|
||||||
@ -667,10 +664,12 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
describe('POST /v1/update', () => {
|
describe('POST /v1/update', () => {
|
||||||
let configStub: SinonStub;
|
let configStub: SinonStub;
|
||||||
let targetUpdateSpy: SinonSpy;
|
let targetUpdateSpy: SinonSpy;
|
||||||
|
let readyForUpdatesStub: SinonStub;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
configStub = stub(config, 'get');
|
configStub = stub(config, 'get');
|
||||||
targetUpdateSpy = spy(TargetState, 'update');
|
targetUpdateSpy = spy(TargetState, 'update');
|
||||||
|
readyForUpdatesStub = stub(apiBinder, 'isReadyForUpdates').returns(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -680,6 +679,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
after(() => {
|
after(() => {
|
||||||
configStub.restore();
|
configStub.restore();
|
||||||
targetUpdateSpy.restore();
|
targetUpdateSpy.restore();
|
||||||
|
readyForUpdatesStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 204 with no parameters', async () => {
|
it('returns 204 with no parameters', async () => {
|
||||||
@ -689,7 +689,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/update')
|
.post('/v1/update')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/update [204 Response]'].statusCode);
|
.expect(sampleResponses.V1.POST['/update [204 Response]'].statusCode);
|
||||||
// Check that TargetState.update was called
|
// Check that TargetState.update was called
|
||||||
expect(targetUpdateSpy).to.be.called;
|
expect(targetUpdateSpy).to.be.called;
|
||||||
@ -704,7 +704,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.post('/v1/update')
|
.post('/v1/update')
|
||||||
.send({ force: true })
|
.send({ force: true })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/update [204 Response]'].statusCode);
|
.expect(sampleResponses.V1.POST['/update [204 Response]'].statusCode);
|
||||||
// Check that TargetState.update was called
|
// Check that TargetState.update was called
|
||||||
expect(targetUpdateSpy).to.be.called;
|
expect(targetUpdateSpy).to.be.called;
|
||||||
@ -718,7 +718,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/update')
|
.post('/v1/update')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/update [202 Response]'].statusCode);
|
.expect(sampleResponses.V1.POST['/update [202 Response]'].statusCode);
|
||||||
// Check that TargetState.update was not called
|
// Check that TargetState.update was not called
|
||||||
expect(targetUpdateSpy).to.not.be.called;
|
expect(targetUpdateSpy).to.not.be.called;
|
||||||
@ -731,7 +731,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/blink')
|
.post('/v1/blink')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/blink'].statusCode)
|
.expect(sampleResponses.V1.POST['/blink'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(
|
expect(response.body).to.deep.equal(
|
||||||
@ -751,7 +751,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/blink')
|
.post('/v1/blink')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(blinkStartSpy.callCount).to.equal(1);
|
expect(blinkStartSpy.callCount).to.equal(1);
|
||||||
clock.tick(15000);
|
clock.tick(15000);
|
||||||
@ -773,13 +773,16 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/regenerate-api-key')
|
.post('/v1/regenerate-api-key')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/regenerate-api-key'].statusCode)
|
.expect(sampleResponses.V1.POST['/regenerate-api-key'].statusCode)
|
||||||
.then((response) => {
|
.then((response) =>
|
||||||
|
Promise.all([response, deviceApi.getGlobalApiKey()]),
|
||||||
|
)
|
||||||
|
.then(([response, globalApiKey]) => {
|
||||||
expect(response.body).to.deep.equal(
|
expect(response.body).to.deep.equal(
|
||||||
sampleResponses.V1.POST['/regenerate-api-key'].body,
|
sampleResponses.V1.POST['/regenerate-api-key'].body,
|
||||||
);
|
);
|
||||||
expect(response.text).to.equal(apiKeys.cloudApiKey);
|
expect(response.text).to.equal(globalApiKey);
|
||||||
newKey = response.text;
|
newKey = response.text;
|
||||||
expect(refreshKeySpy.callCount).to.equal(1);
|
expect(refreshKeySpy.callCount).to.equal(1);
|
||||||
});
|
});
|
||||||
@ -795,7 +798,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('expires old API key after generating new key', async () => {
|
it('expires old API key after generating new key', async () => {
|
||||||
const oldKey: string = apiKeys.cloudApiKey;
|
const oldKey: string = await deviceApi.getGlobalApiKey();
|
||||||
|
|
||||||
await request
|
await request
|
||||||
.post('/v1/regenerate-api-key')
|
.post('/v1/regenerate-api-key')
|
||||||
@ -816,7 +819,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.post('/v1/regenerate-api-key')
|
.post('/v1/regenerate-api-key')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(reportStateSpy.callCount).to.equal(1);
|
expect(reportStateSpy.callCount).to.equal(1);
|
||||||
// Further reportCurrentState tests should be in 05-device-state.spec.ts,
|
// Further reportCurrentState tests should be in 05-device-state.spec.ts,
|
||||||
@ -876,7 +879,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(hostnameProxyRes.statusCode)
|
.expect(hostnameProxyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(hostnameProxyRes.body);
|
expect(response.body).to.deep.equal(hostnameProxyRes.body);
|
||||||
@ -889,7 +892,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(hostnameOnlyRes.statusCode)
|
.expect(hostnameOnlyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(hostnameOnlyRes.body);
|
expect(response.body).to.deep.equal(hostnameOnlyRes.body);
|
||||||
@ -902,7 +905,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(503);
|
.expect(503);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -977,7 +980,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { hostname: 'foobaz' } })
|
.send({ network: { hostname: 'foobaz' } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(423);
|
.expect(423);
|
||||||
|
|
||||||
expect(updateLock.lock).to.be.calledOnce;
|
expect(updateLock.lock).to.be.calledOnce;
|
||||||
@ -986,7 +989,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body.network.hostname).to.deep.equal(
|
expect(response.body.network.hostname).to.deep.equal(
|
||||||
'foobardevice',
|
'foobardevice',
|
||||||
@ -1006,7 +1009,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { hostname: 'foobaz' }, force: true })
|
.send({ network: { hostname: 'foobaz' }, force: true })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(updateLock.lock).to.be.calledOnce;
|
expect(updateLock.lock).to.be.calledOnce;
|
||||||
@ -1015,7 +1018,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body.network.hostname).to.deep.equal('foobaz');
|
expect(response.body.network.hostname).to.deep.equal('foobaz');
|
||||||
});
|
});
|
||||||
@ -1039,7 +1042,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send(patchBody)
|
.send(patchBody)
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
validatePatchResponse(response);
|
validatePatchResponse(response);
|
||||||
@ -1055,7 +1058,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(patchBody);
|
expect(response.body).to.deep.equal(patchBody);
|
||||||
});
|
});
|
||||||
@ -1077,7 +1080,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send(patchBody)
|
.send(patchBody)
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
validatePatchResponse(response);
|
validatePatchResponse(response);
|
||||||
@ -1089,7 +1092,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(patchBody);
|
expect(response.body).to.deep.equal(patchBody);
|
||||||
});
|
});
|
||||||
@ -1103,7 +1106,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { hostname: '' } })
|
.send({ network: { hostname: '' } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
validatePatchResponse(response);
|
validatePatchResponse(response);
|
||||||
@ -1119,7 +1122,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const uuidHostname = await config
|
const uuidHostname = await config
|
||||||
.get('uuid')
|
.get('uuid')
|
||||||
@ -1136,7 +1139,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { proxy: {} } })
|
.send({ network: { proxy: {} } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
validatePatchResponse(response);
|
validatePatchResponse(response);
|
||||||
@ -1156,7 +1159,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(hostnameOnlyRes.statusCode)
|
.expect(hostnameOnlyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(hostnameOnlyRes.body);
|
expect(response.body).to.deep.equal(hostnameOnlyRes.body);
|
||||||
@ -1183,7 +1186,10 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { proxy: { [key]: value } } })
|
.send({ network: { proxy: { [key]: value } } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
`Bearer ${await deviceApi.getGlobalApiKey()}`,
|
||||||
|
)
|
||||||
.expect(
|
.expect(
|
||||||
sampleResponses.V1.PATCH['/host/device-config'].statusCode,
|
sampleResponses.V1.PATCH['/host/device-config'].statusCode,
|
||||||
)
|
)
|
||||||
@ -1199,7 +1205,10 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
`Bearer ${await deviceApi.getGlobalApiKey()}`,
|
||||||
|
)
|
||||||
.expect(hostnameProxyRes.statusCode)
|
.expect(hostnameProxyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal({
|
expect(response.body).to.deep.equal({
|
||||||
@ -1233,7 +1242,10 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { proxy: { [key]: value } } })
|
.send({ network: { proxy: { [key]: value } } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
`Bearer ${await deviceApi.getGlobalApiKey()}`,
|
||||||
|
)
|
||||||
.expect(
|
.expect(
|
||||||
sampleResponses.V1.PATCH['/host/device-config'].statusCode,
|
sampleResponses.V1.PATCH['/host/device-config'].statusCode,
|
||||||
)
|
)
|
||||||
@ -1247,7 +1259,10 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set(
|
||||||
|
'Authorization',
|
||||||
|
`Bearer ${await deviceApi.getGlobalApiKey()}`,
|
||||||
|
)
|
||||||
.expect(hostnameProxyRes.statusCode)
|
.expect(hostnameProxyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal({
|
expect(response.body).to.deep.equal({
|
||||||
@ -1281,7 +1296,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { proxy: { [key]: invalidProxyReqs[key] } } })
|
.send({ network: { proxy: { [key]: invalidProxyReqs[key] } } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (key === 'type') {
|
if (key === 'type') {
|
||||||
@ -1308,7 +1323,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { proxy: { noProxy: ['1.2.3.4/5'] } } })
|
.send({ network: { proxy: { noProxy: ['1.2.3.4/5'] } } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
validatePatchResponse(response);
|
validatePatchResponse(response);
|
||||||
@ -1325,7 +1340,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(hostnameProxyRes.statusCode)
|
.expect(hostnameProxyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal({
|
expect(response.body).to.deep.equal({
|
||||||
@ -1346,7 +1361,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: { proxy: { noProxy: [] } } })
|
.send({ network: { proxy: { noProxy: [] } } })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
validatePatchResponse(response);
|
validatePatchResponse(response);
|
||||||
@ -1363,7 +1378,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(hostnameProxyRes.statusCode)
|
.expect(hostnameProxyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal({
|
expect(response.body).to.deep.equal({
|
||||||
@ -1387,7 +1402,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({ network: {} })
|
.send({ network: {} })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
.expect(sampleResponses.V1.PATCH['/host/device-config'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
validatePatchResponse(response);
|
validatePatchResponse(response);
|
||||||
@ -1399,7 +1414,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v1/device/host-config')
|
.get('/v1/device/host-config')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(hostnameProxyRes.statusCode)
|
.expect(hostnameProxyRes.statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(hostnameProxyRes.body);
|
expect(response.body).to.deep.equal(hostnameProxyRes.body);
|
||||||
@ -1411,7 +1426,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.patch('/v1/device/host-config')
|
.patch('/v1/device/host-config')
|
||||||
.send({})
|
.send({})
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(logWarnStub).to.have.been.calledWith(
|
expect(logWarnStub).to.have.been.calledWith(
|
||||||
@ -1430,7 +1445,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
.post('/v1/purge')
|
.post('/v1/purge')
|
||||||
.send({})
|
.send({})
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(
|
.expect(
|
||||||
sampleResponses.V1.POST['/purge [400 Invalid/missing appId]']
|
sampleResponses.V1.POST['/purge [400 Invalid/missing appId]']
|
||||||
.statusCode,
|
.statusCode,
|
||||||
@ -1443,14 +1458,17 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('purges the /data directory with valid appId', async () => {
|
it('purges the /data directory with valid appId', async () => {
|
||||||
const doPurgeStub: SinonStub = stub(deviceAPI, 'doPurge').resolves();
|
const doPurgeStub: SinonStub = stub(
|
||||||
|
deviceAPIActions,
|
||||||
|
'doPurge',
|
||||||
|
).resolves();
|
||||||
|
|
||||||
await mockedDockerode.testWithData({ containers, images }, async () => {
|
await mockedDockerode.testWithData({ containers, images }, async () => {
|
||||||
await request
|
await request
|
||||||
.post('/v1/purge')
|
.post('/v1/purge')
|
||||||
.send({ appId: 2 })
|
.send({ appId: 2 })
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V1.POST['/purge [200]'].statusCode)
|
.expect(sampleResponses.V1.POST['/purge [200]'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(
|
expect(response.body).to.deep.equal(
|
||||||
@ -1467,7 +1485,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
|
|||||||
// Generate a new scoped key to call the endpoint, as mocked
|
// Generate a new scoped key to call the endpoint, as mocked
|
||||||
// appId = 2 services are all in the global scope and thus
|
// appId = 2 services are all in the global scope and thus
|
||||||
// resolve to true for any isScoped check
|
// resolve to true for any isScoped check
|
||||||
const scopedKey = await apiKeys.generateScopedKey(
|
const scopedKey = await deviceApi.generateScopedKey(
|
||||||
2,
|
2,
|
||||||
containers[0].serviceName,
|
containers[0].serviceName,
|
||||||
);
|
);
|
||||||
|
@ -7,10 +7,10 @@ import sampleResponses = require('~/test-data/device-api-responses.json');
|
|||||||
import mockedAPI = require('~/test-lib/mocked-device-api');
|
import mockedAPI = require('~/test-lib/mocked-device-api');
|
||||||
import * as apiBinder from '~/src/api-binder';
|
import * as apiBinder from '~/src/api-binder';
|
||||||
import * as deviceState from '~/src/device-state';
|
import * as deviceState from '~/src/device-state';
|
||||||
import SupervisorAPI from '~/src/supervisor-api';
|
import SupervisorAPI from '~/src/device-api';
|
||||||
|
import * as deviceApi from '~/src/device-api';
|
||||||
import * as serviceManager from '~/src/compose/service-manager';
|
import * as serviceManager from '~/src/compose/service-manager';
|
||||||
import * as images from '~/src/compose/images';
|
import * as images from '~/src/compose/images';
|
||||||
import * as apiKeys from '~/lib/api-keys';
|
|
||||||
import * as config from '~/src/config';
|
import * as config from '~/src/config';
|
||||||
import * as updateLock from '~/lib/update-lock';
|
import * as updateLock from '~/lib/update-lock';
|
||||||
import * as targetStateCache from '~/src/device-state/target-state-cache';
|
import * as targetStateCache from '~/src/device-state/target-state-cache';
|
||||||
@ -45,9 +45,6 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
mockedAPI.mockedOptions.timeout,
|
mockedAPI.mockedOptions.timeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a scoped key
|
|
||||||
await apiKeys.initialized();
|
|
||||||
await apiKeys.generateCloudKey();
|
|
||||||
serviceManagerMock = stub(serviceManager, 'getAll').resolves([]);
|
serviceManagerMock = stub(serviceManager, 'getAll').resolves([]);
|
||||||
imagesMock = stub(images, 'getState').resolves([]);
|
imagesMock = stub(images, 'getState').resolves([]);
|
||||||
|
|
||||||
@ -85,7 +82,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v2/device/vpn')
|
.get('/v2/device/vpn')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(sampleResponses.V2.GET['/device/vpn'].statusCode)
|
.expect(sampleResponses.V2.GET['/device/vpn'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -101,7 +98,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v2/applications/1/state')
|
.get('/v2/applications/1/state')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V2.GET['/applications/1/state'].statusCode)
|
.expect(sampleResponses.V2.GET['/applications/1/state'].statusCode)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -115,7 +112,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v2/applications/123invalid/state')
|
.get('/v2/applications/123invalid/state')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(
|
.expect(
|
||||||
sampleResponses.V2.GET['/applications/123invalid/state'].statusCode,
|
sampleResponses.V2.GET['/applications/123invalid/state'].statusCode,
|
||||||
@ -131,7 +128,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
await request
|
await request
|
||||||
.get('/v2/applications/9000/state')
|
.get('/v2/applications/9000/state')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
|
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
|
||||||
.expect(sampleResponses.V2.GET['/applications/9000/state'].statusCode)
|
.expect(sampleResponses.V2.GET['/applications/9000/state'].statusCode)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.body).to.deep.equal(
|
expect(response.body).to.deep.equal(
|
||||||
@ -142,7 +139,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
|
|
||||||
describe('Scoped API Keys', () => {
|
describe('Scoped API Keys', () => {
|
||||||
it('returns 409 because app is out of scope of the key', async () => {
|
it('returns 409 because app is out of scope of the key', async () => {
|
||||||
const apiKey = await apiKeys.generateScopedKey(3, 'main');
|
const apiKey = await deviceApi.generateScopedKey(3, 'main');
|
||||||
await request
|
await request
|
||||||
.get('/v2/applications/2/state')
|
.get('/v2/applications/2/state')
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
@ -164,7 +161,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
|
|
||||||
it('should return scoped application', async () => {
|
it('should return scoped application', async () => {
|
||||||
// Create scoped key for application
|
// Create scoped key for application
|
||||||
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
|
const appScopedKey = await deviceApi.generateScopedKey(1658654, 'main');
|
||||||
// Setup device conditions
|
// Setup device conditions
|
||||||
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
|
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
|
||||||
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
|
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
|
||||||
@ -188,7 +185,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
|
|
||||||
it('should return no application info due to lack of scope', async () => {
|
it('should return no application info due to lack of scope', async () => {
|
||||||
// Create scoped key for wrong application
|
// Create scoped key for wrong application
|
||||||
const appScopedKey = await apiKeys.generateScopedKey(1, 'main');
|
const appScopedKey = await deviceApi.generateScopedKey(1, 'main');
|
||||||
// Setup device conditions
|
// Setup device conditions
|
||||||
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
|
serviceManagerMock.resolves([mockedAPI.mockService({ appId: 1658654 })]);
|
||||||
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
|
imagesMock.resolves([mockedAPI.mockImage({ appId: 1658654 })]);
|
||||||
@ -211,7 +208,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
|
|
||||||
it('should return success when device has no applications', async () => {
|
it('should return success when device has no applications', async () => {
|
||||||
// Create scoped key for any application
|
// Create scoped key for any application
|
||||||
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
|
const appScopedKey = await deviceApi.generateScopedKey(1658654, 'main');
|
||||||
// Setup device conditions
|
// Setup device conditions
|
||||||
serviceManagerMock.resolves([]);
|
serviceManagerMock.resolves([]);
|
||||||
imagesMock.resolves([]);
|
imagesMock.resolves([]);
|
||||||
@ -234,7 +231,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
|
|
||||||
it('should only return 1 application when N > 1 applications on device', async () => {
|
it('should only return 1 application when N > 1 applications on device', async () => {
|
||||||
// Create scoped key for application
|
// Create scoped key for application
|
||||||
const appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
|
const appScopedKey = await deviceApi.generateScopedKey(1658654, 'main');
|
||||||
// Setup device conditions
|
// Setup device conditions
|
||||||
serviceManagerMock.resolves([
|
serviceManagerMock.resolves([
|
||||||
mockedAPI.mockService({ appId: 1658654 }),
|
mockedAPI.mockService({ appId: 1658654 }),
|
||||||
@ -330,7 +327,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// Create scoped key for application
|
// Create scoped key for application
|
||||||
appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
|
appScopedKey = await deviceApi.generateScopedKey(1658654, 'main');
|
||||||
|
|
||||||
// Mock target state cache
|
// Mock target state cache
|
||||||
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
|
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
|
||||||
@ -439,7 +436,7 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
// Create scoped key for application
|
// Create scoped key for application
|
||||||
appScopedKey = await apiKeys.generateScopedKey(1658654, 'main');
|
appScopedKey = await deviceApi.generateScopedKey(1658654, 'main');
|
||||||
|
|
||||||
// Mock target state cache
|
// Mock target state cache
|
||||||
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
|
targetStateCacheMock = stub(targetStateCache, 'getTargetApp');
|
||||||
|
@ -31,6 +31,5 @@ fs.writeFileSync(
|
|||||||
fs.readFileSync('./test/data/testconfig.json'),
|
fs.readFileSync('./test/data/testconfig.json'),
|
||||||
);
|
);
|
||||||
|
|
||||||
import '~/test-lib/mocked-dbus';
|
|
||||||
import '~/test-lib/mocked-dockerode';
|
import '~/test-lib/mocked-dockerode';
|
||||||
import '~/test-lib/mocked-iptables';
|
import '~/test-lib/mocked-iptables';
|
||||||
|
70
test/lib/legacy-mocha-hooks.ts
Normal file
70
test/lib/legacy-mocha-hooks.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// TODO: Remove this file when all legacy tests have migrated to unit/integration.
|
||||||
|
|
||||||
|
import { stub, SinonStub } from 'sinon';
|
||||||
|
import * as dbus from 'dbus';
|
||||||
|
import { Error as DBusError, DBusInterface } from 'dbus';
|
||||||
|
import { initialized } from '~/src/lib/dbus';
|
||||||
|
|
||||||
|
let getBusStub: SinonStub;
|
||||||
|
|
||||||
|
export const mochaHooks = {
|
||||||
|
async beforeAll() {
|
||||||
|
getBusStub = stub(dbus, 'getBus').returns({
|
||||||
|
getInterface: (
|
||||||
|
serviceName: string,
|
||||||
|
_objectPath: string,
|
||||||
|
_interfaceName: string,
|
||||||
|
interfaceCb: (err: null | DBusError, iface: DBusInterface) => void,
|
||||||
|
) => {
|
||||||
|
if (/systemd/.test(serviceName)) {
|
||||||
|
interfaceCb(null, {
|
||||||
|
StartUnit: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
RestartUnit: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
StopUnit: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
EnableUnitFiles: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
DisableUnitFiles: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
GetUnit: (
|
||||||
|
_unitName: string,
|
||||||
|
getUnitCb: (err: null | Error, unitPath: string) => void,
|
||||||
|
) => {
|
||||||
|
getUnitCb(null, 'this is the unit path');
|
||||||
|
},
|
||||||
|
Get: (
|
||||||
|
_unitName: string,
|
||||||
|
_property: string,
|
||||||
|
getCb: (err: null | Error, value: unknown) => void,
|
||||||
|
) => {
|
||||||
|
getCb(null, 'this is the value');
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
} else {
|
||||||
|
interfaceCb(null, {
|
||||||
|
Reboot: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
PowerOff: () => {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as dbus.DBusConnection);
|
||||||
|
|
||||||
|
// Initialize dbus module before any tests are run so any further tests
|
||||||
|
// that interface with lib/dbus use the stubbed busses above.
|
||||||
|
await initialized();
|
||||||
|
},
|
||||||
|
afterAll() {
|
||||||
|
getBusStub.restore();
|
||||||
|
},
|
||||||
|
};
|
@ -12,7 +12,7 @@ const api: express.Express & {
|
|||||||
} = express();
|
} = express();
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
api.use(require('body-parser').json());
|
api.use(express.json());
|
||||||
|
|
||||||
api.balenaBackend = {
|
api.balenaBackend = {
|
||||||
currentId: 1,
|
currentId: 1,
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
import * as dbus from 'dbus';
|
|
||||||
import { Error as DBusError, DBusInterface } from 'dbus';
|
|
||||||
import { stub } from 'sinon';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Because lib/dbus invokes dbus.getBus on module import,
|
|
||||||
* getBus needs to be stubbed at the root level due how JS
|
|
||||||
* `require` works. lib/dbus interfaces with the systemd and
|
|
||||||
* logind interfaces, which expose the unit methods below.
|
|
||||||
*
|
|
||||||
* There should be no need to un-stub dbus.getBus at any point
|
|
||||||
* during testing, since we never want to interact with the actual
|
|
||||||
* dbus system socket in the test environment.
|
|
||||||
*
|
|
||||||
* To test interaction with lib/dbus, import lib/dbus into the test suite
|
|
||||||
* and stub the necessary methods, as you would with any other module.
|
|
||||||
*/
|
|
||||||
stub(dbus, 'getBus').returns({
|
|
||||||
getInterface: (
|
|
||||||
serviceName: string,
|
|
||||||
_objectPath: string,
|
|
||||||
_interfaceName: string,
|
|
||||||
interfaceCb: (err: null | DBusError, iface: DBusInterface) => void,
|
|
||||||
) => {
|
|
||||||
if (/systemd/.test(serviceName)) {
|
|
||||||
interfaceCb(null, {
|
|
||||||
StartUnit: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
RestartUnit: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
StopUnit: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
EnableUnitFiles: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
DisableUnitFiles: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
GetUnit: (
|
|
||||||
_unitName: string,
|
|
||||||
getUnitCb: (err: null | Error, unitPath: string) => void,
|
|
||||||
) => {
|
|
||||||
getUnitCb(null, 'this is the unit path');
|
|
||||||
},
|
|
||||||
Get: (
|
|
||||||
_unitName: string,
|
|
||||||
_property: string,
|
|
||||||
getCb: (err: null | Error, value: unknown) => void,
|
|
||||||
) => {
|
|
||||||
getCb(null, 'this is the value');
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
} else {
|
|
||||||
interfaceCb(null, {
|
|
||||||
Reboot: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
PowerOff: () => {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
} as any);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} as dbus.DBusConnection);
|
|
@ -1,18 +1,16 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { Router } from 'express';
|
|
||||||
import rewire = require('rewire');
|
import rewire = require('rewire');
|
||||||
|
|
||||||
import { unlinkAll } from '~/lib/fs-utils';
|
import { unlinkAll } from '~/lib/fs-utils';
|
||||||
import * as applicationManager from '~/src/compose/application-manager';
|
|
||||||
import * as serviceManager from '~/src/compose/service-manager';
|
import * as serviceManager from '~/src/compose/service-manager';
|
||||||
import * as volumeManager from '~/src/compose/volume-manager';
|
import * as volumeManager from '~/src/compose/volume-manager';
|
||||||
import * as commitStore from '~/src/compose/commit';
|
import * as commitStore from '~/src/compose/commit';
|
||||||
import * as config from '~/src/config';
|
import * as config from '~/src/config';
|
||||||
import * as db from '~/src/db';
|
import * as db from '~/src/db';
|
||||||
import { createV1Api } from '~/src/device-api/v1';
|
import * as v1 from '~/src/device-api/v1';
|
||||||
import { createV2Api } from '~/src/device-api/v2';
|
import * as v2 from '~/src/device-api/v2';
|
||||||
import * as deviceState from '~/src/device-state';
|
import * as deviceState from '~/src/device-state';
|
||||||
import SupervisorAPI from '~/src/supervisor-api';
|
import SupervisorAPI from '~/src/device-api';
|
||||||
import { Service } from '~/src/compose/service';
|
import { Service } from '~/src/compose/service';
|
||||||
import { Image } from '~/src/compose/images';
|
import { Image } from '~/src/compose/images';
|
||||||
|
|
||||||
@ -135,7 +133,7 @@ async function create(
|
|||||||
|
|
||||||
// Create SupervisorAPI
|
// Create SupervisorAPI
|
||||||
const api = new SupervisorAPI({
|
const api = new SupervisorAPI({
|
||||||
routers: [deviceState.router, buildRoutes()],
|
routers: [v1.router, v2.router],
|
||||||
healthchecks,
|
healthchecks,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -173,21 +171,10 @@ async function initConfig(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRoutes(): Router {
|
|
||||||
// Add to existing apiBinder router (it contains additional middleware and endpoints)
|
|
||||||
const router = apiBinder.router;
|
|
||||||
// Add V1 routes
|
|
||||||
createV1Api(applicationManager.router);
|
|
||||||
// Add V2 routes
|
|
||||||
createV2Api(applicationManager.router);
|
|
||||||
// Return modified Router
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TO-DO: Create a cleaner way to restore previous values.
|
// TO-DO: Create a cleaner way to restore previous values.
|
||||||
const originalVolGetAll = volumeManager.getAllByAppId;
|
const originalVolGetAll = volumeManager.getAllByAppId;
|
||||||
const originalSvcGetStatus = serviceManager.getState;
|
const originalSvcGetStatus = serviceManager.getState;
|
||||||
const originalReadyForUpdates = apiBinder.__get__('readyForUpdates');
|
const originalReadyForUpdates = apiBinder.isReadyForUpdates();
|
||||||
|
|
||||||
function setupStubs() {
|
function setupStubs() {
|
||||||
apiBinder.__set__('readyForUpdates', true);
|
apiBinder.__set__('readyForUpdates', true);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user