mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-01 23:30:48 +00:00
Convert target state types to io-ts for better validation
This simplifies target state validation and improves validation messages. Change-type: patch
This commit is contained in:
parent
ca7c22d854
commit
f6692ab918
@ -23,7 +23,7 @@
|
|||||||
"release": "tsc --project tsconfig.release.json && mv build/src/* build",
|
"release": "tsc --project tsconfig.release.json && mv build/src/* build",
|
||||||
"packagejson:copy": "cp package.json build/",
|
"packagejson:copy": "cp package.json build/",
|
||||||
"testitems:copy": "cp -r test/data build/test/",
|
"testitems:copy": "cp -r test/data build/test/",
|
||||||
"sync": "ts-node sync/sync.ts",
|
"sync": "ts-node --files sync/sync.ts",
|
||||||
"clean": "rimraf build"
|
"clean": "rimraf build"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -32,7 +32,7 @@ import { createV2Api } from '../device-api/v2';
|
|||||||
import { CompositionStep, generateStep } from './composition-steps';
|
import { CompositionStep, generateStep } from './composition-steps';
|
||||||
import {
|
import {
|
||||||
InstancedAppState,
|
InstancedAppState,
|
||||||
TargetApplications,
|
TargetApps,
|
||||||
DeviceStatus,
|
DeviceStatus,
|
||||||
DeviceReportFields,
|
DeviceReportFields,
|
||||||
TargetState,
|
TargetState,
|
||||||
@ -463,13 +463,13 @@ export async function executeStep(
|
|||||||
|
|
||||||
// FIXME: This shouldn't be in this module
|
// FIXME: This shouldn't be in this module
|
||||||
export async function setTarget(
|
export async function setTarget(
|
||||||
apps: TargetApplications,
|
apps: TargetApps,
|
||||||
dependent: TargetState['dependent'],
|
dependent: TargetState['dependent'],
|
||||||
source: string,
|
source: string,
|
||||||
maybeTrx?: Transaction,
|
maybeTrx?: Transaction,
|
||||||
) {
|
) {
|
||||||
const setInTransaction = async (
|
const setInTransaction = async (
|
||||||
$filteredApps: TargetApplications,
|
$filteredApps: TargetApps,
|
||||||
trx: Transaction,
|
trx: Transaction,
|
||||||
) => {
|
) => {
|
||||||
await dbFormat.setApps($filteredApps, source, trx);
|
await dbFormat.setApps($filteredApps, source, trx);
|
||||||
@ -534,7 +534,7 @@ export async function setTarget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTargetApps(): Promise<TargetApplications> {
|
export async function getTargetApps(): Promise<TargetApps> {
|
||||||
const apps = await dbFormat.getTargetJson();
|
const apps = await dbFormat.getTargetJson();
|
||||||
|
|
||||||
// Whilst it may make sense here to return the target state generated from the
|
// Whilst it may make sense here to return the target state generated from the
|
||||||
|
@ -242,6 +242,7 @@ export async function create(service: Service) {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this seems a bit late to be checking this
|
||||||
const deviceName = await config.get('name');
|
const deviceName = await config.get('name');
|
||||||
if (!isValidDeviceName(deviceName)) {
|
if (!isValidDeviceName(deviceName)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -4,6 +4,8 @@ import { EventEmitter } from 'events';
|
|||||||
import * as express from 'express';
|
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 Reporter from 'io-ts-reporters';
|
||||||
|
|
||||||
import prettyMs = require('pretty-ms');
|
import prettyMs = require('pretty-ms');
|
||||||
|
|
||||||
@ -42,52 +44,15 @@ import * as apiKeys from './lib/api-keys';
|
|||||||
|
|
||||||
const disallowedHostConfigPatchFields = ['local_ip', 'local_port'];
|
const disallowedHostConfigPatchFields = ['local_ip', 'local_port'];
|
||||||
|
|
||||||
function validateLocalState(state: any): asserts state is TargetState['local'] {
|
function parseTargetState(state: unknown): TargetState {
|
||||||
if (state.name != null) {
|
const res = TargetState.decode(state);
|
||||||
if (!validation.isValidShortText(state.name)) {
|
|
||||||
throw new Error('Invalid device name');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.apps == null || !validation.isValidAppsObject(state.apps)) {
|
|
||||||
throw new Error('Invalid apps');
|
|
||||||
}
|
|
||||||
if (state.config == null || !validation.isValidEnv(state.config)) {
|
|
||||||
throw new Error('Invalid device configuration');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateDependentState(
|
if (isRight(res)) {
|
||||||
state: any,
|
return res.right;
|
||||||
): asserts state is TargetState['dependent'] {
|
|
||||||
if (
|
|
||||||
state.apps != null &&
|
|
||||||
!validation.isValidDependentAppsObject(state.apps)
|
|
||||||
) {
|
|
||||||
throw new Error('Invalid dependent apps');
|
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
state.devices != null &&
|
|
||||||
!validation.isValidDependentDevicesObject(state.devices)
|
|
||||||
) {
|
|
||||||
throw new Error('Invalid dependent devices');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateState(state: any): asserts state is TargetState {
|
const errors = ['Invalid target state.'].concat(Reporter.report(res));
|
||||||
if (!_.isObject(state)) {
|
throw new Error(errors.join('\n'));
|
||||||
throw new Error('State must be an object');
|
|
||||||
}
|
|
||||||
// these any typings seem unnecessary but the `isObject`
|
|
||||||
// call above tells typescript that state is of type
|
|
||||||
// `object` - which apparently does not allow any fields
|
|
||||||
// to be accessed
|
|
||||||
if (!_.isObject((state as any).local)) {
|
|
||||||
throw new Error('Local state must be an object');
|
|
||||||
}
|
|
||||||
validateLocalState((state as any).local);
|
|
||||||
if ((state as any).dependent != null) {
|
|
||||||
return validateDependentState((state as any).dependent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (refactor): This shouldn't be here, and instead should be part of the other
|
// TODO (refactor): This shouldn't be here, and instead should be part of the other
|
||||||
@ -502,7 +467,8 @@ export async function setTarget(target: TargetState, localSource?: boolean) {
|
|||||||
}
|
}
|
||||||
failedUpdates = 0;
|
failedUpdates = 0;
|
||||||
|
|
||||||
validateState(target);
|
// This will throw if target state is invalid
|
||||||
|
target = parseTargetState(target);
|
||||||
|
|
||||||
globalEventBus.getInstance().emit('targetStateChanged', target);
|
globalEventBus.getInstance().emit('targetStateChanged', target);
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@ import * as images from '../compose/images';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
InstancedAppState,
|
InstancedAppState,
|
||||||
TargetApplication,
|
TargetApp,
|
||||||
TargetApplications,
|
TargetApps,
|
||||||
TargetApplicationService,
|
TargetService,
|
||||||
} from '../types/state';
|
} from '../types/state';
|
||||||
import { checkInt } from '../lib/validation';
|
import { checkInt } from '../lib/validation';
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ export async function getApps(): Promise<InstancedAppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function setApps(
|
export async function setApps(
|
||||||
apps: { [appId: number]: TargetApplication },
|
apps: { [appId: number]: TargetApp },
|
||||||
source: string,
|
source: string,
|
||||||
trx?: db.Transaction,
|
trx?: db.Transaction,
|
||||||
) {
|
) {
|
||||||
@ -72,9 +72,9 @@ export async function setApps(
|
|||||||
await targetStateCache.setTargetApps(dbApps, trx);
|
await targetStateCache.setTargetApps(dbApps, trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTargetJson(): Promise<TargetApplications> {
|
export async function getTargetJson(): Promise<TargetApps> {
|
||||||
const dbApps = await getDBEntry();
|
const dbApps = await getDBEntry();
|
||||||
const apps: TargetApplications = {};
|
const apps: TargetApps = {};
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dbApps.map(async (app) => {
|
dbApps.map(async (app) => {
|
||||||
const parsedServices = JSON.parse(app.services);
|
const parsedServices = JSON.parse(app.services);
|
||||||
@ -82,8 +82,7 @@ export async function getTargetJson(): Promise<TargetApplications> {
|
|||||||
const services = _(parsedServices)
|
const services = _(parsedServices)
|
||||||
.keyBy('serviceId')
|
.keyBy('serviceId')
|
||||||
.mapValues(
|
.mapValues(
|
||||||
(svc: TargetApplicationService) =>
|
(svc: TargetService) => _.omit(svc, 'commit') as TargetService,
|
||||||
_.omit(svc, 'commit') as TargetApplicationService,
|
|
||||||
)
|
)
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
@ -96,7 +95,7 @@ export async function getTargetJson(): Promise<TargetApplications> {
|
|||||||
networks: JSON.parse(app.networks),
|
networks: JSON.parse(app.networks),
|
||||||
volumes: JSON.parse(app.volumes),
|
volumes: JSON.parse(app.volumes),
|
||||||
// We can add this cast because it's required in the db
|
// We can add this cast because it's required in the db
|
||||||
} as TargetApplication;
|
} as TargetApp;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return apps;
|
return apps;
|
||||||
|
@ -23,18 +23,15 @@ import {
|
|||||||
import { docker } from '../lib/docker-utils';
|
import { docker } from '../lib/docker-utils';
|
||||||
import { exec, pathExistsOnHost, mkdirp } from '../lib/fs-utils';
|
import { exec, pathExistsOnHost, mkdirp } from '../lib/fs-utils';
|
||||||
import { log } from '../lib/supervisor-console';
|
import { log } from '../lib/supervisor-console';
|
||||||
import type {
|
import type { AppsJsonFormat, TargetApp, TargetState } from '../types/state';
|
||||||
AppsJsonFormat,
|
|
||||||
TargetApplication,
|
|
||||||
TargetState,
|
|
||||||
} from '../types/state';
|
|
||||||
import type { DatabaseApp } from '../device-state/target-state-cache';
|
import type { DatabaseApp } from '../device-state/target-state-cache';
|
||||||
|
import { ShortString } from '../types';
|
||||||
|
|
||||||
export const defaultLegacyVolume = () => 'resin-data';
|
export const defaultLegacyVolume = () => 'resin-data';
|
||||||
|
|
||||||
export function singleToMulticontainerApp(
|
export function singleToMulticontainerApp(
|
||||||
app: Dictionary<any>,
|
app: Dictionary<any>,
|
||||||
): TargetApplication & { appId: string } {
|
): TargetApp & { appId: string } {
|
||||||
const environment: Dictionary<string> = {};
|
const environment: Dictionary<string> = {};
|
||||||
for (const key in app.env) {
|
for (const key in app.env) {
|
||||||
if (!/^RESIN_/.test(key)) {
|
if (!/^RESIN_/.test(key)) {
|
||||||
@ -44,7 +41,7 @@ export function singleToMulticontainerApp(
|
|||||||
|
|
||||||
const { appId } = app;
|
const { appId } = app;
|
||||||
const conf = app.config != null ? app.config : {};
|
const conf = app.config != null ? app.config : {};
|
||||||
const newApp: TargetApplication & { appId: string } = {
|
const newApp: TargetApp & { appId: string } = {
|
||||||
appId: appId.toString(),
|
appId: appId.toString(),
|
||||||
commit: app.commit,
|
commit: app.commit,
|
||||||
name: app.name,
|
name: app.name,
|
||||||
@ -72,7 +69,7 @@ export function singleToMulticontainerApp(
|
|||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
'1': {
|
'1': {
|
||||||
appId,
|
appId,
|
||||||
serviceName: 'main',
|
serviceName: 'main' as ShortString,
|
||||||
imageId: 1,
|
imageId: 1,
|
||||||
commit: app.commit,
|
commit: app.commit,
|
||||||
releaseId: 1,
|
releaseId: 1,
|
||||||
@ -132,7 +129,7 @@ export async function normaliseLegacyDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
let services: Array<TargetApplication['services']['']>;
|
let services: Array<TargetApp['services']['']>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
services = JSON.parse(app.services);
|
services = JSON.parse(app.services);
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
|
import { isRight } from 'fp-ts/lib/Either';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { inspect } from 'util';
|
import { DeviceName } from '../types';
|
||||||
|
|
||||||
import { TargetState } from '../types/state';
|
|
||||||
import { EnvVarObject, LabelObject } from '../types';
|
|
||||||
|
|
||||||
import log from './supervisor-console';
|
|
||||||
|
|
||||||
export interface CheckIntOptions {
|
export interface CheckIntOptions {
|
||||||
positive?: boolean;
|
positive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENV_VAR_KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
||||||
const LABEL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9\.\-]*$/;
|
|
||||||
const NUMERALS_REGEX = /^-?[0-9]+\.?0*$/; // Allows trailing 0 decimals
|
const NUMERALS_REGEX = /^-?[0-9]+\.?0*$/; // Allows trailing 0 decimals
|
||||||
const TRUTHY = ['1', 'true', true, 'on', 1];
|
const TRUTHY = ['1', 'true', true, 'on', 1];
|
||||||
const FALSEY = ['0', 'false', false, 'off', 0];
|
const FALSEY = ['0', 'false', false, 'off', 0];
|
||||||
@ -93,434 +86,8 @@ export function checkFalsey(v: unknown): boolean {
|
|||||||
return FALSEY.includes(v as any);
|
return FALSEY.includes(v as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
export function isValidDeviceName(v: unknown): v is DeviceName {
|
||||||
* isValidShortText
|
return isRight(DeviceName.decode(v));
|
||||||
*
|
|
||||||
* Check that the input string is definitely a string,
|
|
||||||
* and has a length which is less than 255
|
|
||||||
*/
|
|
||||||
export function isValidShortText(t: unknown): boolean {
|
|
||||||
return _.isString(t) && t.length <= 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* isValidEnv
|
|
||||||
*
|
|
||||||
* Given a env var object, check types and values for the keys
|
|
||||||
* and values
|
|
||||||
*/
|
|
||||||
export function isValidEnv(obj: EnvVarObject): boolean {
|
|
||||||
if (!_.isObject(obj)) {
|
|
||||||
log.debug(
|
|
||||||
`Non-object passed to validation.isValidEnv\nobj: ${inspect(obj)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.every(obj, (val, key) => {
|
|
||||||
if (!isValidShortText(key)) {
|
|
||||||
log.debug(
|
|
||||||
`Non-valid short text env var key passed to validation.isValidEnv\nKey: ${inspect(
|
|
||||||
key,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ENV_VAR_KEY_REGEX.test(key)) {
|
|
||||||
log.debug(
|
|
||||||
`Invalid env var key passed to validation.isValidEnv\nKey: ${inspect(
|
|
||||||
key,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isString(val)) {
|
|
||||||
log.debug(
|
|
||||||
`Non-string value passed to validation.isValidEnv\nValue: ${inspect(
|
|
||||||
key,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* isValidLabelsObject
|
|
||||||
*
|
|
||||||
* Given a labels object, test the types and values for validity
|
|
||||||
*/
|
|
||||||
export function isValidLabelsObject(obj: LabelObject): boolean {
|
|
||||||
if (!_.isObject(obj)) {
|
|
||||||
log.debug(
|
|
||||||
`Non-object passed to validation.isValidLabelsObject\nobj: ${inspect(
|
|
||||||
obj,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.every(obj, (val, key) => {
|
|
||||||
if (!isValidShortText(key)) {
|
|
||||||
log.debug(
|
|
||||||
`Non-valid short text label key passed to validation.isValidLabelsObject\nKey: ${inspect(
|
|
||||||
key,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!LABEL_NAME_REGEX.test(key)) {
|
|
||||||
log.debug(
|
|
||||||
`Invalid label name passed to validation.isValidLabelsObject\nKey: ${inspect(
|
|
||||||
key,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isString(val)) {
|
|
||||||
log.debug(
|
|
||||||
`Non-string value passed to validation.isValidLabelsObject\nValue: ${inspect(
|
|
||||||
val,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidDeviceName(name: string): boolean {
|
|
||||||
// currently the only disallowed value in a device name is a newline
|
|
||||||
const newline = name.indexOf('\n') !== -1;
|
|
||||||
if (newline) {
|
|
||||||
log.debug(
|
|
||||||
'Newline found in device name. This is invalid and should be removed',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return !newline;
|
|
||||||
}
|
|
||||||
|
|
||||||
function undefinedOrValidEnv(val: EnvVarObject): boolean {
|
|
||||||
return val == null || isValidEnv(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* isValidDependentAppsObject
|
|
||||||
*
|
|
||||||
* Given a dependent apps object from a state endpoint, validate it
|
|
||||||
*
|
|
||||||
* TODO: Type the input
|
|
||||||
*/
|
|
||||||
export function isValidDependentAppsObject(apps: unknown): boolean {
|
|
||||||
if (!_.isObject(apps)) {
|
|
||||||
log.debug(
|
|
||||||
'Non-object passed to validation.isValidDependentAppsObject\nApps:',
|
|
||||||
inspect(apps),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.every(apps, (v, appId) => {
|
|
||||||
const val: TargetState['dependent']['apps'][any] = _.defaults(_.clone(v), {
|
|
||||||
config: undefined,
|
|
||||||
environment: undefined,
|
|
||||||
commit: undefined,
|
|
||||||
image: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isValidShortText(appId) || !checkInt(appId)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid appId passed to validation.isValidDependentAppsObject\nappId:',
|
|
||||||
inspect(appId),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.conformsTo(val, {
|
|
||||||
parentApp: (n: any) => {
|
|
||||||
if (!checkInt(n)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid parentApp passed to validation.isValidDependentAppsObject\nName:',
|
|
||||||
inspect(n),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
name: (n: any) => {
|
|
||||||
if (!isValidShortText(n)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid name passed to validation.isValidDependentAppsObject\nName:',
|
|
||||||
inspect(n),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
image: (i: any) => {
|
|
||||||
if (val.commit != null && !isValidShortText(i)) {
|
|
||||||
log.debug(
|
|
||||||
'Non valid image passed to validation.isValidDependentAppsObject\nImage:',
|
|
||||||
inspect(i),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
commit: (c: any) => {
|
|
||||||
if (c != null && !isValidShortText(c)) {
|
|
||||||
log.debug(
|
|
||||||
'invalid commit passed to validation.isValidDependentAppsObject\nCommit:',
|
|
||||||
inspect(c),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
config: (c: any) => {
|
|
||||||
if (!undefinedOrValidEnv(c)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid config passed to validation.isValidDependentAppsObject\nConfig:',
|
|
||||||
inspect(c),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidService(service: any, serviceId: string): boolean {
|
|
||||||
if (!isValidShortText(serviceId) || !checkInt(serviceId)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid service id passed to validation.isValidService\nService ID:',
|
|
||||||
inspect(serviceId),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.conformsTo(service, {
|
|
||||||
serviceName: (n: any) => {
|
|
||||||
if (!isValidShortText(n)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid service name passed to validation.isValidService\nService Name:',
|
|
||||||
inspect(n),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
image: (i: any) => {
|
|
||||||
if (!isValidShortText(i)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid image passed to validation.isValidService\nImage:',
|
|
||||||
inspect(i),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
environment: (e: any) => {
|
|
||||||
if (!isValidEnv(e)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid env passed to validation.isValidService\nEnvironment:',
|
|
||||||
inspect(e),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
imageId: (i: any) => {
|
|
||||||
if (checkInt(i) == null) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid image id passed to validation.isValidService\nImage ID:',
|
|
||||||
inspect(i),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
labels: (l: any) => {
|
|
||||||
if (!isValidLabelsObject(l)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid labels object passed to validation.isValidService\nLabels:',
|
|
||||||
inspect(l),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* isValidAppsObject
|
|
||||||
*
|
|
||||||
* Given an apps object from the state endpoint, validate the fields and
|
|
||||||
* return whether it's valid.
|
|
||||||
*
|
|
||||||
* TODO: Type the input correctly
|
|
||||||
*/
|
|
||||||
export function isValidAppsObject(obj: any): boolean {
|
|
||||||
if (!_.isObject(obj)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid object passed to validation.isValidAppsObject\nobj:',
|
|
||||||
inspect(obj),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.every(obj, (v, appId) => {
|
|
||||||
if (!isValidShortText(appId) || !checkInt(appId)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid appId passed to validation.isValidAppsObject\nApp ID:',
|
|
||||||
inspect(appId),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove this partial and validate the extra fields
|
|
||||||
const val: Partial<TargetState['local']['apps'][any]> = _.defaults(
|
|
||||||
_.clone(v),
|
|
||||||
{ releaseId: undefined },
|
|
||||||
);
|
|
||||||
|
|
||||||
return _.conformsTo(val, {
|
|
||||||
name: (n: any) => {
|
|
||||||
if (!isValidShortText(n)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid service name passed to validation.isValidAppsObject\nName:',
|
|
||||||
inspect(n),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
releaseId: (r: any) => {
|
|
||||||
if (r != null && checkInt(r) == null) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid releaseId passed to validation.isValidAppsObject\nRelease ID',
|
|
||||||
inspect(r),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
services: (s: any) => {
|
|
||||||
if (!_.isObject(s)) {
|
|
||||||
log.debug(
|
|
||||||
'Non-object service passed to validation.isValidAppsObject\nServices:',
|
|
||||||
inspect(s),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.every(s, (svc, svcId) => {
|
|
||||||
if (!isValidService(svc, svcId)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid service object passed to validation.isValidAppsObject\nService:',
|
|
||||||
inspect(svc),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* isValidDependentDevicesObject
|
|
||||||
*
|
|
||||||
* Validate a dependent devices object from the state endpoint.
|
|
||||||
*/
|
|
||||||
export function isValidDependentDevicesObject(devices: any): boolean {
|
|
||||||
if (!_.isObject(devices)) {
|
|
||||||
log.debug(
|
|
||||||
'Non-object passed to validation.isValidDependentDevicesObject\nDevices:',
|
|
||||||
inspect(devices),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.every(devices, (val, uuid) => {
|
|
||||||
if (!isValidShortText(uuid)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid uuid passed to validation.isValidDependentDevicesObject\nuuid:',
|
|
||||||
inspect(uuid),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.conformsTo(val as TargetState['dependent']['devices'][any], {
|
|
||||||
name: (n: any) => {
|
|
||||||
if (!isValidShortText(n)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid device name passed to validation.isValidDependentDevicesObject\nName:',
|
|
||||||
inspect(n),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
apps: (a: any) => {
|
|
||||||
if (!_.isObject(a)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid apps object passed to validation.isValidDependentDevicesObject\nApps:',
|
|
||||||
inspect(a),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.isEmpty(a)) {
|
|
||||||
log.debug(
|
|
||||||
'Empty object passed to validation.isValidDependentDevicesObject',
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.every(
|
|
||||||
a as TargetState['dependent']['devices'][any]['apps'],
|
|
||||||
(app) => {
|
|
||||||
app = _.defaults(_.clone(app), {
|
|
||||||
config: undefined,
|
|
||||||
environment: undefined,
|
|
||||||
});
|
|
||||||
return _.conformsTo(app, {
|
|
||||||
config: (c: any) => {
|
|
||||||
if (!undefinedOrValidEnv(c)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid config passed to validation.isValidDependentDevicesObject\nConfig:',
|
|
||||||
inspect(c),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
environment: (e: any) => {
|
|
||||||
if (!undefinedOrValidEnv(e)) {
|
|
||||||
log.debug(
|
|
||||||
'Invalid environment passed to validation.isValidDependentDevicesObject\nConfig:',
|
|
||||||
inspect(e),
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,182 @@
|
|||||||
export interface EnvVarObject {
|
import * as t from 'io-ts';
|
||||||
[name: string]: string;
|
import { chain } from 'fp-ts/lib/Either';
|
||||||
}
|
import { pipe } from 'fp-ts/function';
|
||||||
|
|
||||||
export interface LabelObject {
|
/**
|
||||||
[name: string]: string;
|
* A short string is a non null string between
|
||||||
}
|
* 0 and 255 characters
|
||||||
|
*/
|
||||||
|
export const ShortString = new t.Type<string, string>(
|
||||||
|
'ShortString',
|
||||||
|
(i: unknown): i is string => t.string.is(i) && i.length <= 255,
|
||||||
|
(i, c) =>
|
||||||
|
pipe(
|
||||||
|
t.string.validate(i, c),
|
||||||
|
chain((s) =>
|
||||||
|
s.length <= 255
|
||||||
|
? t.success(s)
|
||||||
|
: t.failure(s, c, 'must be at most 255 chars long'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.identity,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note: assigning this type to a string will not throw compilation errorrs.
|
||||||
|
//
|
||||||
|
// e.g. the following will compile without issues.
|
||||||
|
// ```
|
||||||
|
// const x: ShortString = 'a'.repeat(300);
|
||||||
|
// ```
|
||||||
|
export type ShortString = t.TypeOf<typeof ShortString>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string identifier is a string that encodes a
|
||||||
|
* positive integer (an id to be used as a database id)
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* Invalid decimal strings: 'aaa', '0xaaa'
|
||||||
|
* Valid decimal strings: '0', '123'
|
||||||
|
*/
|
||||||
|
export const StringIdentifier = new t.Type<string, string>(
|
||||||
|
'StringIdentifier',
|
||||||
|
(i: unknown): i is string =>
|
||||||
|
t.string.is(i) && !isNaN(+i) && +i === parseInt(i, 10) && +i >= 0,
|
||||||
|
(i, c) =>
|
||||||
|
pipe(
|
||||||
|
t.string.validate(i, c),
|
||||||
|
chain((s) =>
|
||||||
|
!isNaN(+s) && +s === parseInt(s, 10) && +s >= 0
|
||||||
|
? t.success(s)
|
||||||
|
: t.failure(s, c, 'must be be an positive integer'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
);
|
||||||
|
|
||||||
|
export type StringIdentifier = t.TypeOf<typeof StringIdentifier>;
|
||||||
|
|
||||||
|
export const StringOrNumber = t.union([t.number, t.string]);
|
||||||
|
export type StringOrNumber = t.TypeOf<typeof StringOrNumber>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A numeric identifier is any valid identifier encoded as a string or number
|
||||||
|
*/
|
||||||
|
export const NumericIdentifier = new t.Type<number, StringOrNumber>(
|
||||||
|
'NumericIdentifier',
|
||||||
|
(i): i is number =>
|
||||||
|
StringOrNumber.is(i) &&
|
||||||
|
!isNaN(+i) &&
|
||||||
|
+i === parseInt(String(i), 10) &&
|
||||||
|
+i >= 0,
|
||||||
|
(i, c) =>
|
||||||
|
pipe(
|
||||||
|
StringOrNumber.validate(i, c),
|
||||||
|
chain((n) =>
|
||||||
|
!isNaN(+n) && +n === parseInt(String(n), 10) && +n >= 0
|
||||||
|
? t.success(+n)
|
||||||
|
: t.failure(n, c, 'must be be an positive integer'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Number,
|
||||||
|
);
|
||||||
|
export type NumericIdentifier = t.TypeOf<typeof NumericIdentifier>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid variable names are between 0 and 255 characters
|
||||||
|
* and match /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
||||||
|
*/
|
||||||
|
const VAR_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||||
|
|
||||||
|
export const VariableName = new t.Type<string, string>(
|
||||||
|
'VariableName',
|
||||||
|
(s: unknown): s is string => ShortString.is(s) && VAR_NAME_REGEX.test(s),
|
||||||
|
(i, c) =>
|
||||||
|
pipe(
|
||||||
|
ShortString.validate(i, c),
|
||||||
|
chain((s) =>
|
||||||
|
VAR_NAME_REGEX.test(s)
|
||||||
|
? t.success(s)
|
||||||
|
: t.failure(s, c, "may only contain alphanumeric chars plus '_'"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.identity,
|
||||||
|
);
|
||||||
|
export type VariableName = t.TypeOf<typeof VariableName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid label names are between 0 and 255 characters
|
||||||
|
* and match /^[a-zA-Z][a-zA-Z0-9\.\-]*$/
|
||||||
|
*/
|
||||||
|
const LABEL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9\.\-]*$/;
|
||||||
|
|
||||||
|
export const LabelName = new t.Type<string, string>(
|
||||||
|
'LabelName',
|
||||||
|
(s: unknown): s is string => ShortString.is(s) && LABEL_NAME_REGEX.test(s),
|
||||||
|
(i, c) =>
|
||||||
|
pipe(
|
||||||
|
ShortString.validate(i, c),
|
||||||
|
chain((s) =>
|
||||||
|
LABEL_NAME_REGEX.test(s)
|
||||||
|
? t.success(s)
|
||||||
|
: t.failure(
|
||||||
|
s,
|
||||||
|
c,
|
||||||
|
"may only contain alphanumeric chars plus '-' and '.'",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.identity,
|
||||||
|
);
|
||||||
|
export type LabelName = t.TypeOf<typeof LabelName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An env var object is a dictionary with valid variables as keys
|
||||||
|
*/
|
||||||
|
export const EnvVarObject = t.record(VariableName, t.string);
|
||||||
|
export type EnvVarObject = t.TypeOf<typeof EnvVarObject>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An env var object is a dictionary with valid labels as keys
|
||||||
|
*/
|
||||||
|
export const LabelObject = t.record(LabelName, t.string);
|
||||||
|
export type LabelObject = t.TypeOf<typeof LabelObject>;
|
||||||
|
|
||||||
|
// Valid docker container and volume name according to
|
||||||
|
// https://github.com/moby/moby/blob/04c6f09fbdf60c7765cc4cb78883faaa9d971fa5/daemon/daemon.go#L56
|
||||||
|
// [a-zA-Z0-9][a-zA-Z0-9_.-]
|
||||||
|
const DOCKER_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_\.\-]*$/;
|
||||||
|
export const DockerName = new t.Type<string, string>(
|
||||||
|
'DockerName',
|
||||||
|
(s: unknown): s is string => ShortString.is(s) && DOCKER_NAME_REGEX.test(s),
|
||||||
|
(i, c) =>
|
||||||
|
pipe(
|
||||||
|
ShortString.validate(i, c),
|
||||||
|
chain((s) =>
|
||||||
|
DOCKER_NAME_REGEX.test(s)
|
||||||
|
? t.success(s)
|
||||||
|
: t.failure(s, c, 'only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.identity,
|
||||||
|
);
|
||||||
|
export type DockerName = t.TypeOf<typeof DockerName>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device name can have any characters except '\n'
|
||||||
|
*/
|
||||||
|
export const DeviceName = new t.Type<string, string>(
|
||||||
|
'DeviceName',
|
||||||
|
(i: unknown): i is string => ShortString.is(i) && i.indexOf('\n') === -1,
|
||||||
|
(i, c) =>
|
||||||
|
pipe(
|
||||||
|
ShortString.validate(i, c),
|
||||||
|
chain((s) =>
|
||||||
|
s.indexOf('\n') === -1
|
||||||
|
? t.success(s)
|
||||||
|
: t.failure(s, c, 'must not contain newline chars'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
t.identity,
|
||||||
|
);
|
||||||
|
|
||||||
|
export type DeviceName = t.TypeOf<typeof DeviceName>;
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
|
import * as t from 'io-ts';
|
||||||
|
|
||||||
// TODO: move all these exported types to ../compose/types
|
// TODO: move all these exported types to ../compose/types
|
||||||
import { ComposeNetworkConfig } from '../compose/types/network';
|
import { ComposeNetworkConfig } from '../compose/types/network';
|
||||||
import { ServiceComposeConfig } from '../compose/types/service';
|
import { ServiceComposeConfig } from '../compose/types/service';
|
||||||
import { ComposeVolumeConfig } from '../compose/volume';
|
import { ComposeVolumeConfig } from '../compose/volume';
|
||||||
|
|
||||||
import { EnvVarObject, LabelObject } from './basic';
|
import {
|
||||||
|
DockerName,
|
||||||
|
EnvVarObject,
|
||||||
|
LabelObject,
|
||||||
|
StringIdentifier,
|
||||||
|
NumericIdentifier,
|
||||||
|
ShortString,
|
||||||
|
DeviceName,
|
||||||
|
} from './basic';
|
||||||
|
|
||||||
import App from '../compose/app';
|
import App from '../compose/app';
|
||||||
|
|
||||||
@ -48,70 +58,176 @@ export interface DeviceStatus {
|
|||||||
commit?: string;
|
commit?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Define this with io-ts so we can perform validation
|
// Return a type with a default value
|
||||||
// on the target state from the api, local mode, and preload
|
const withDefault = <T extends t.Any>(
|
||||||
export interface TargetState {
|
type: T,
|
||||||
local: {
|
defaultValue: t.TypeOf<T>,
|
||||||
name: string;
|
): t.Type<t.TypeOf<T>> =>
|
||||||
config: EnvVarObject;
|
new t.Type(
|
||||||
apps: {
|
type.name,
|
||||||
[appId: string]: {
|
type.is,
|
||||||
name: string;
|
(v, c) => type.validate(!!v ? v : defaultValue, c),
|
||||||
commit?: string;
|
type.encode,
|
||||||
releaseId?: number;
|
);
|
||||||
services: {
|
|
||||||
[serviceId: string]: {
|
|
||||||
labels: LabelObject;
|
|
||||||
imageId: number;
|
|
||||||
serviceName: string;
|
|
||||||
image: string;
|
|
||||||
running?: boolean;
|
|
||||||
environment: Dictionary<string>;
|
|
||||||
contract?: Dictionary<any>;
|
|
||||||
} & ServiceComposeConfig;
|
|
||||||
};
|
|
||||||
volumes: Dictionary<Partial<ComposeVolumeConfig>>;
|
|
||||||
networks: Dictionary<Partial<ComposeNetworkConfig>>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
dependent: {
|
|
||||||
apps: {
|
|
||||||
[appId: string]: {
|
|
||||||
name: string;
|
|
||||||
parentApp: number;
|
|
||||||
config: EnvVarObject;
|
|
||||||
releaseId?: number;
|
|
||||||
imageId?: number;
|
|
||||||
commit?: string;
|
|
||||||
image?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
devices: {
|
|
||||||
[uuid: string]: {
|
|
||||||
name: string;
|
|
||||||
apps: {
|
|
||||||
[id: string]: {
|
|
||||||
config: EnvVarObject;
|
|
||||||
environment: EnvVarObject;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LocalTargetState = TargetState['local'];
|
/**
|
||||||
export type TargetApplications = LocalTargetState['apps'];
|
* Utility function to return a io-ts type from a native typescript
|
||||||
export type TargetApplication = LocalTargetState['apps'][0];
|
* type.
|
||||||
export type TargetApplicationService = TargetApplication['services'][0];
|
*
|
||||||
export type AppsJsonFormat = Omit<TargetState['local'], 'name'> & {
|
* **IMPORTANT**: This will NOT validate the type, just allow to combine the generated
|
||||||
pinDevice?: boolean;
|
* type with other io-ts types.
|
||||||
apps: {
|
*
|
||||||
// The releaseId/commit are required for preloading
|
* Please do NOT export. This is a placeholder while updating other related
|
||||||
[id: string]: Required<TargetState['local']['apps'][string]>;
|
* types to io-ts
|
||||||
};
|
*
|
||||||
};
|
* Example:
|
||||||
|
* ```
|
||||||
|
* export
|
||||||
|
*
|
||||||
|
* type MyType = { one: string };
|
||||||
|
* const MyType = fromType<MyType>('MyType'); // both name and generic value are required :(
|
||||||
|
* const OtherType = t.type({name: t.string, other: MyType });
|
||||||
|
* OtherType.decode({name: 'john', other: {one: 1}); // will decode to true
|
||||||
|
*
|
||||||
|
* type OtherType = t.TypeOf<typeof OtherType>; // will have the correct type definition
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const fromType = <T extends object>(name: string) =>
|
||||||
|
new t.Type<T>(
|
||||||
|
name,
|
||||||
|
(input: unknown): input is T => typeof input === 'object' && input !== null,
|
||||||
|
(input, context) =>
|
||||||
|
typeof input === 'object' && input !== null
|
||||||
|
? (t.success(input) as t.Validation<T>)
|
||||||
|
: t.failure(
|
||||||
|
input,
|
||||||
|
context,
|
||||||
|
`Expected value to be an object of type ${name}, got: ${input}`,
|
||||||
|
),
|
||||||
|
t.identity,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TargetService = t.intersection([
|
||||||
|
t.type({
|
||||||
|
serviceName: DockerName,
|
||||||
|
imageId: NumericIdentifier,
|
||||||
|
image: ShortString,
|
||||||
|
environment: EnvVarObject,
|
||||||
|
labels: LabelObject,
|
||||||
|
}),
|
||||||
|
t.partial({
|
||||||
|
running: withDefault(t.boolean, true),
|
||||||
|
contract: t.record(t.string, t.unknown),
|
||||||
|
}),
|
||||||
|
// This will not be validated
|
||||||
|
// TODO: convert ServiceComposeConfig to a io-ts type
|
||||||
|
fromType<ServiceComposeConfig>('ServiceComposition'),
|
||||||
|
]);
|
||||||
|
export type TargetService = t.TypeOf<typeof TargetService>;
|
||||||
|
|
||||||
|
const TargetApp = t.intersection(
|
||||||
|
[
|
||||||
|
t.type({
|
||||||
|
name: ShortString,
|
||||||
|
services: withDefault(t.record(StringIdentifier, TargetService), {}),
|
||||||
|
volumes: withDefault(
|
||||||
|
t.record(
|
||||||
|
DockerName,
|
||||||
|
// TargetVolume format will NOT be validated
|
||||||
|
// TODO: convert ComposeVolumeConfig to a io-ts type
|
||||||
|
fromType<Partial<ComposeVolumeConfig>>('Volume'),
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
networks: withDefault(
|
||||||
|
t.record(
|
||||||
|
DockerName,
|
||||||
|
// TargetNetwork format will NOT be validated
|
||||||
|
// TODO: convert ComposeVolumeConfig to a io-ts type
|
||||||
|
fromType<Partial<ComposeNetworkConfig>>('Network'),
|
||||||
|
),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
t.partial({
|
||||||
|
commit: ShortString,
|
||||||
|
releaseId: NumericIdentifier,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'App',
|
||||||
|
);
|
||||||
|
export type TargetApp = t.TypeOf<typeof TargetApp>;
|
||||||
|
|
||||||
|
export const TargetApps = t.record(StringIdentifier, TargetApp);
|
||||||
|
export type TargetApps = t.TypeOf<typeof TargetApps>;
|
||||||
|
|
||||||
|
const DependentApp = t.intersection(
|
||||||
|
[
|
||||||
|
t.type({
|
||||||
|
name: ShortString,
|
||||||
|
parentApp: NumericIdentifier,
|
||||||
|
config: EnvVarObject,
|
||||||
|
}),
|
||||||
|
t.partial({
|
||||||
|
releaseId: NumericIdentifier,
|
||||||
|
imageId: NumericIdentifier,
|
||||||
|
commit: ShortString,
|
||||||
|
image: ShortString,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
'DependentApp',
|
||||||
|
);
|
||||||
|
|
||||||
|
const DependentDevice = t.type(
|
||||||
|
{
|
||||||
|
name: ShortString, // device uuid
|
||||||
|
apps: t.record(
|
||||||
|
StringIdentifier,
|
||||||
|
t.type({ config: EnvVarObject, environment: EnvVarObject }),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'DependentDevice',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Although the original types for dependent apps and dependent devices was a dictionary,
|
||||||
|
// proxyvisor.js accepts both a dictionary and an array. Unfortunately
|
||||||
|
// the CLI sends an array, thus the types need to accept both
|
||||||
|
const DependentApps = t.union([
|
||||||
|
t.array(DependentApp),
|
||||||
|
t.record(StringIdentifier, DependentApp),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DependentDevices = t.union([
|
||||||
|
t.array(DependentDevice),
|
||||||
|
t.record(StringIdentifier, DependentDevice),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const TargetState = t.type({
|
||||||
|
local: t.type({
|
||||||
|
name: DeviceName,
|
||||||
|
config: EnvVarObject,
|
||||||
|
apps: TargetApps,
|
||||||
|
}),
|
||||||
|
dependent: t.type({
|
||||||
|
apps: DependentApps,
|
||||||
|
devices: DependentDevices,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
export type TargetState = t.TypeOf<typeof TargetState>;
|
||||||
|
|
||||||
|
const TargetAppWithRelease = t.intersection([
|
||||||
|
TargetApp,
|
||||||
|
t.type({ commit: t.string, releaseId: NumericIdentifier }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const AppsJsonFormat = t.intersection([
|
||||||
|
t.type({
|
||||||
|
config: EnvVarObject,
|
||||||
|
apps: t.record(StringIdentifier, TargetAppWithRelease),
|
||||||
|
}),
|
||||||
|
t.partial({ pinDevice: t.boolean }),
|
||||||
|
]);
|
||||||
|
export type AppsJsonFormat = t.TypeOf<typeof AppsJsonFormat>;
|
||||||
|
|
||||||
export type InstancedAppState = { [appId: number]: App };
|
export type InstancedAppState = { [appId: number]: App };
|
||||||
|
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import * as validation from '../src/lib/validation';
|
import { isRight } from 'fp-ts/lib/Either';
|
||||||
|
import {
|
||||||
|
StringIdentifier,
|
||||||
|
ShortString,
|
||||||
|
DeviceName,
|
||||||
|
NumericIdentifier,
|
||||||
|
TargetApps,
|
||||||
|
} from '../src/types';
|
||||||
|
|
||||||
const almostTooLongText = _.times(255, () => 'a').join('');
|
import * as validation from '../src/lib/validation';
|
||||||
|
|
||||||
describe('validation', () => {
|
describe('validation', () => {
|
||||||
describe('checkBooleanish', () => {
|
describe('checkBooleanish', () => {
|
||||||
@ -144,175 +151,319 @@ describe('validation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isValidShortText', () => {
|
describe('short string', () => {
|
||||||
it('returns true for a short text', () => {
|
it('accepts strings below 255 chars', () => {
|
||||||
expect(validation.isValidShortText('foo')).to.equal(true);
|
expect(isRight(ShortString.decode('aaaa'))).to.be.true;
|
||||||
expect(validation.isValidShortText('')).to.equal(true);
|
expect(isRight(ShortString.decode('1234'))).to.be.true;
|
||||||
expect(validation.isValidShortText(almostTooLongText)).to.equal(true);
|
expect(isRight(ShortString.decode('some longish alphanumeric text 1236')))
|
||||||
|
.to.be.true;
|
||||||
|
expect(isRight(ShortString.decode('a'.repeat(255)))).to.be.true;
|
||||||
});
|
});
|
||||||
it('returns false for a text longer than 255 characters', () =>
|
|
||||||
expect(validation.isValidShortText(almostTooLongText + 'a')).to.equal(
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
it('returns false when passed a non-string', () => {
|
it('rejects non strings or strings longer than 255 chars', () => {
|
||||||
expect(validation.isValidShortText({})).to.equal(false);
|
expect(isRight(ShortString.decode(null))).to.be.false;
|
||||||
expect(validation.isValidShortText(1)).to.equal(false);
|
expect(isRight(ShortString.decode(undefined))).to.be.false;
|
||||||
expect(validation.isValidShortText(null)).to.equal(false);
|
expect(isRight(ShortString.decode([]))).to.be.false;
|
||||||
expect(validation.isValidShortText(undefined)).to.equal(false);
|
expect(isRight(ShortString.decode(1234))).to.be.false;
|
||||||
|
expect(isRight(ShortString.decode('a'.repeat(256)))).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isValidAppsObject', () => {
|
describe('device name', () => {
|
||||||
it('returns true for a valid object', () => {
|
it('accepts strings below 255 chars', () => {
|
||||||
const apps = {
|
expect(isRight(DeviceName.decode('aaaa'))).to.be.true;
|
||||||
'1234': {
|
expect(isRight(DeviceName.decode('1234'))).to.be.true;
|
||||||
name: 'something',
|
expect(isRight(DeviceName.decode('some longish alphanumeric text 1236')))
|
||||||
releaseId: 123,
|
.to.be.true;
|
||||||
commit: 'bar',
|
expect(isRight(DeviceName.decode('a'.repeat(255)))).to.be.true;
|
||||||
services: {
|
|
||||||
'45': {
|
|
||||||
serviceName: 'bazbaz',
|
|
||||||
imageId: 34,
|
|
||||||
image: 'foo',
|
|
||||||
environment: {},
|
|
||||||
labels: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validation.isValidAppsObject(apps)).to.equal(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false with an invalid environment', () => {
|
it('rejects non strings or strings longer than 255 chars', () => {
|
||||||
const apps = {
|
expect(isRight(DeviceName.decode(null))).to.be.false;
|
||||||
'1234': {
|
expect(isRight(DeviceName.decode(undefined))).to.be.false;
|
||||||
name: 'something',
|
expect(isRight(DeviceName.decode([]))).to.be.false;
|
||||||
releaseId: 123,
|
expect(isRight(DeviceName.decode(1234))).to.be.false;
|
||||||
commit: 'bar',
|
expect(isRight(DeviceName.decode('a'.repeat(256)))).to.be.false;
|
||||||
services: {
|
|
||||||
'45': {
|
|
||||||
serviceName: 'bazbaz',
|
|
||||||
imageId: 34,
|
|
||||||
image: 'foo',
|
|
||||||
environment: { ' baz': 'bat' },
|
|
||||||
labels: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validation.isValidAppsObject(apps)).to.equal(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false with an invalid appId', () => {
|
it('rejects strings with new lines', () => {
|
||||||
const apps = {
|
expect(isRight(DeviceName.decode('\n'))).to.be.false;
|
||||||
boo: {
|
expect(isRight(DeviceName.decode('aaaa\nbbbb'))).to.be.false;
|
||||||
name: 'something',
|
expect(isRight(DeviceName.decode('\n' + 'a'.repeat(254)))).to.be.false;
|
||||||
releaseId: 123,
|
expect(isRight(DeviceName.decode('\n' + 'a'.repeat(255)))).to.be.false;
|
||||||
commit: 'bar',
|
|
||||||
services: {
|
|
||||||
'45': {
|
|
||||||
serviceName: 'bazbaz',
|
|
||||||
imageId: 34,
|
|
||||||
image: 'foo',
|
|
||||||
environment: {},
|
|
||||||
labels: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validation.isValidAppsObject(apps)).to.equal(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true with a missing releaseId', () => {
|
|
||||||
const apps = {
|
|
||||||
'1234': {
|
|
||||||
name: 'something',
|
|
||||||
services: {
|
|
||||||
'45': {
|
|
||||||
serviceName: 'bazbaz',
|
|
||||||
imageId: 34,
|
|
||||||
image: 'foo',
|
|
||||||
environment: {},
|
|
||||||
labels: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validation.isValidAppsObject(apps)).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false with an invalid releaseId', () => {
|
|
||||||
const apps = {
|
|
||||||
'1234': {
|
|
||||||
name: 'something',
|
|
||||||
releaseId: '123a',
|
|
||||||
services: {
|
|
||||||
'45': {
|
|
||||||
serviceName: 'bazbaz',
|
|
||||||
imageId: 34,
|
|
||||||
image: 'foo',
|
|
||||||
environment: {},
|
|
||||||
labels: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validation.isValidAppsObject(apps)).to.equal(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isValidDependentDevicesObject', () => {
|
describe('string identifier', () => {
|
||||||
it('returns true for a valid object', () => {
|
it('accepts integer strings as input', () => {
|
||||||
const devices: Dictionary<any> = {};
|
expect(isRight(StringIdentifier.decode('0'))).to.be.true;
|
||||||
devices[almostTooLongText] = {
|
expect(isRight(StringIdentifier.decode('1234'))).to.be.true;
|
||||||
name: 'foo',
|
expect(isRight(StringIdentifier.decode('51564189199'))).to.be.true;
|
||||||
apps: {
|
|
||||||
'234': {
|
|
||||||
config: { bar: 'baz' },
|
|
||||||
environment: { dead: 'beef' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
expect(validation.isValidDependentDevicesObject(devices)).to.equal(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false with a missing apps object', () => {
|
it('rejects non strings or non numeric strings', () => {
|
||||||
const devices = {
|
expect(isRight(StringIdentifier.decode(null))).to.be.false;
|
||||||
abcd1234: {
|
expect(isRight(StringIdentifier.decode(undefined))).to.be.false;
|
||||||
name: 'foo',
|
expect(isRight(StringIdentifier.decode([1]))).to.be.false;
|
||||||
},
|
expect(isRight(StringIdentifier.decode('[1]'))).to.be.false;
|
||||||
};
|
expect(isRight(StringIdentifier.decode(12345))).to.be.false;
|
||||||
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false);
|
expect(isRight(StringIdentifier.decode(-12345))).to.be.false;
|
||||||
|
expect(isRight(StringIdentifier.decode('aaaa'))).to.be.false;
|
||||||
|
expect(isRight(StringIdentifier.decode('-125'))).to.be.false;
|
||||||
|
expect(isRight(StringIdentifier.decode('0xffff'))).to.be.false;
|
||||||
|
expect(isRight(StringIdentifier.decode('1544.333'))).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false with an invalid environment', () => {
|
it('decodes to a string', () => {
|
||||||
const devices = {
|
expect(StringIdentifier.decode('12345'))
|
||||||
abcd1234: {
|
.to.have.property('right')
|
||||||
name: 'foo',
|
.that.equals('12345');
|
||||||
apps: {
|
});
|
||||||
'234': {
|
});
|
||||||
config: { bar: 'baz' },
|
|
||||||
environment: { dead: 1 },
|
describe('numeric identifier', () => {
|
||||||
|
it('accepts integers and integer strings as input', () => {
|
||||||
|
expect(isRight(NumericIdentifier.decode('0'))).to.be.true;
|
||||||
|
expect(isRight(NumericIdentifier.decode('1234'))).to.be.true;
|
||||||
|
expect(isRight(NumericIdentifier.decode(1234))).to.be.true;
|
||||||
|
expect(isRight(NumericIdentifier.decode(51564189199))).to.be.true;
|
||||||
|
expect(isRight(NumericIdentifier.decode('51564189199'))).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non strings or non numeric strings', () => {
|
||||||
|
expect(isRight(NumericIdentifier.decode(null))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode(undefined))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode([1]))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode('[1]'))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode('aaaa'))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode('-125'))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode('0xffff'))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode('1544.333'))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode(1544.333))).to.be.false;
|
||||||
|
expect(isRight(NumericIdentifier.decode(-1544.333))).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes to a number', () => {
|
||||||
|
expect(NumericIdentifier.decode('12345'))
|
||||||
|
.to.have.property('right')
|
||||||
|
.that.equals(12345);
|
||||||
|
expect(NumericIdentifier.decode(12345))
|
||||||
|
.to.have.property('right')
|
||||||
|
.that.equals(12345);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('target apps', () => {
|
||||||
|
it('accept valid target apps', () => {
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
'1234': {
|
||||||
|
name: 'something',
|
||||||
|
services: {},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
),
|
||||||
};
|
'accepts apps with no no release id or commit',
|
||||||
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false);
|
).to.be.true;
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
'1234': {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: 123,
|
||||||
|
commit: 'bar',
|
||||||
|
services: {},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
'accepts apps with no services',
|
||||||
|
).to.be.true;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
'1234': {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: 123,
|
||||||
|
commit: 'bar',
|
||||||
|
services: {
|
||||||
|
'45': {
|
||||||
|
serviceName: 'bazbaz',
|
||||||
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: { MY_SERVICE_ENV_VAR: '123' },
|
||||||
|
labels: { 'io.balena.features.supervisor-api': 'true' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
volumes: {},
|
||||||
|
networks: {},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
'accepts apps with a service',
|
||||||
|
).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if the uuid is too long', () => {
|
it('rejects app with invalid environment', () => {
|
||||||
const devices: Dictionary<any> = {};
|
expect(
|
||||||
devices[almostTooLongText + 'a'] = {
|
isRight(
|
||||||
name: 'foo',
|
TargetApps.decode({
|
||||||
apps: {
|
'1234': {
|
||||||
'234': {
|
name: 'something',
|
||||||
config: { bar: 'baz' },
|
releaseId: 123,
|
||||||
environment: { dead: 'beef' },
|
commit: 'bar',
|
||||||
},
|
services: {
|
||||||
},
|
'45': {
|
||||||
};
|
serviceName: 'bazbaz',
|
||||||
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false);
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: { ' baz': 'bat' },
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects app with invalid labels', () => {
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
'1234': {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: 123,
|
||||||
|
commit: 'bar',
|
||||||
|
services: {
|
||||||
|
'45': {
|
||||||
|
serviceName: 'bazbaz',
|
||||||
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: {},
|
||||||
|
labels: { ' not a valid #name': 'label value' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an invalid appId', () => {
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
boo: {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: 123,
|
||||||
|
commit: 'bar',
|
||||||
|
services: {
|
||||||
|
'45': {
|
||||||
|
serviceName: 'bazbaz',
|
||||||
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: {},
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a commit that is too long', () => {
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
boo: {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: 123,
|
||||||
|
commit: 'a'.repeat(256),
|
||||||
|
services: {
|
||||||
|
'45': {
|
||||||
|
serviceName: 'bazbaz',
|
||||||
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: {},
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
|
|
||||||
|
it('rejects a service with an invalid docker name', () => {
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
boo: {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: 123,
|
||||||
|
commit: 'a'.repeat(256),
|
||||||
|
services: {
|
||||||
|
'45': {
|
||||||
|
serviceName: ' not a valid name',
|
||||||
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: {},
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects a commit that is too long', () => {
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
boo: {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: 123,
|
||||||
|
commit: 'a'.repeat(256),
|
||||||
|
services: {
|
||||||
|
'45': {
|
||||||
|
serviceName: 'bazbaz',
|
||||||
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: {},
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects app with an invalid releaseId', () => {
|
||||||
|
expect(
|
||||||
|
isRight(
|
||||||
|
TargetApps.decode({
|
||||||
|
boo: {
|
||||||
|
name: 'something',
|
||||||
|
releaseId: '123aaa',
|
||||||
|
commit: 'bar',
|
||||||
|
services: {
|
||||||
|
'45': {
|
||||||
|
serviceName: 'bazbaz',
|
||||||
|
imageId: 34,
|
||||||
|
image: 'foo',
|
||||||
|
environment: {},
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ import * as images from '../src/compose/images';
|
|||||||
import App from '../src/compose/app';
|
import App from '../src/compose/app';
|
||||||
import Service from '../src/compose/service';
|
import Service from '../src/compose/service';
|
||||||
import Network from '../src/compose/network';
|
import Network from '../src/compose/network';
|
||||||
import { TargetApplication } from '../src/types/state';
|
import { TargetApp } from '../src/types/state';
|
||||||
|
|
||||||
function getDefaultNetworks(appId: number) {
|
function getDefaultNetworks(appId: number) {
|
||||||
return {
|
return {
|
||||||
@ -123,7 +123,7 @@ describe('DB Format', () => {
|
|||||||
|
|
||||||
it('should write target states to the database', async () => {
|
it('should write target states to the database', async () => {
|
||||||
const target = await import('./data/state-endpoints/simple.json');
|
const target = await import('./data/state-endpoints/simple.json');
|
||||||
const dbApps: { [appId: number]: TargetApplication } = {};
|
const dbApps: { [appId: number]: TargetApp } = {};
|
||||||
dbApps[1234] = {
|
dbApps[1234] = {
|
||||||
...target.local.apps[1234],
|
...target.local.apps[1234],
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user