Merge pull request #786 from balena-io/update-linting

Update linting
This commit is contained in:
Page- 2018-11-02 15:10:41 +00:00 committed by GitHub
commit 3db260aa5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1470 additions and 1084 deletions

10
.lintstagedrc Normal file
View File

@ -0,0 +1,10 @@
{
"*.coffee": [
"resin-lint"
],
"*.ts": [
"prettier --config ./node_modules/resin-lint/config/.prettierrc --write",
"resin-lint --typescript --no-prettier",
"git add"
],
}

View File

@ -10,7 +10,11 @@
"scripts": { "scripts": {
"start": "./entry.sh", "start": "./entry.sh",
"build": "webpack", "build": "webpack",
"lint": "resin-lint --typescript src/ test/", "precommit": "lint-staged",
"prettify": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"{src,test,typings}/**/*.ts\"",
"lint:coffee": "resin-lint src/ test/",
"lint:typescript": "resin-lint --typescript src/ test/ typings/ && tsc --noEmit",
"lint": "npm run lint:coffee && npm run lint:typescript",
"test": "npm run lint && JUNIT_REPORT_PATH=report.xml mocha --exit -r ts-node/register -r coffee-script/register -r register-coffee-coverage test/*.{js,coffee} && npm run coverage", "test": "npm run lint && JUNIT_REPORT_PATH=report.xml mocha --exit -r ts-node/register -r coffee-script/register -r register-coffee-coverage test/*.{js,coffee} && npm run coverage",
"test:fast": "mocha --exit -r ts-node/register -r coffee-script/register test/*.{js,coffee}", "test:fast": "mocha --exit -r ts-node/register -r coffee-script/register test/*.{js,coffee}",
"test:build": "tsc && coffee -m -c -o build . && cp -r test/data build/test/ && cp -r src/migrations build/src && cp package.json build", "test:build": "tsc && coffee -m -c -o build . && cp -r test/data build/test/ && cp -r src/migrations build/src && cp package.json build",
@ -52,9 +56,11 @@
"duration-js": "^4.0.0", "duration-js": "^4.0.0",
"event-stream": "^3.0.20", "event-stream": "^3.0.20",
"express": "^4.0.0", "express": "^4.0.0",
"husky": "^1.1.3",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"json-mask": "^0.3.8", "json-mask": "^0.3.8",
"knex": "~0.12.3", "knex": "~0.12.3",
"lint-staged": "^8.0.4",
"lockfile": "^1.0.1", "lockfile": "^1.0.1",
"lodash": "^4.16.3", "lodash": "^4.16.3",
"log-timestamp": "^0.1.2", "log-timestamp": "^0.1.2",
@ -68,9 +74,10 @@
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"null-loader": "^0.1.1", "null-loader": "^0.1.1",
"pinejs-client": "^2.4.0", "pinejs-client": "^2.4.0",
"prettier": "^1.14.3",
"register-coffee-coverage": "0.0.1", "register-coffee-coverage": "0.0.1",
"request": "^2.51.0", "request": "^2.51.0",
"resin-lint": "^1.5.7", "resin-lint": "^2.0.1",
"resin-register-device": "^3.0.0", "resin-register-device": "^3.0.0",
"resin-sync": "^9.3.0", "resin-sync": "^9.3.0",
"resumable-request": "^2.0.0", "resumable-request": "^2.0.0",

View File

@ -810,9 +810,9 @@ module.exports = class ApplicationManager extends EventEmitter
img = _.find(available, { dockerImageId: service.config.image, imageId: service.imageId }) ? _.find(available, { dockerImageId: service.config.image }) img = _.find(available, { dockerImageId: service.config.image, imageId: service.imageId }) ? _.find(available, { dockerImageId: service.config.image })
return _.omit(img, [ 'dockerImageId', 'id' ]) return _.omit(img, [ 'dockerImageId', 'id' ])
allImageDockerIdsForTargetApp = (app) -> allImageDockerIdsForTargetApp = (app) ->
_(app.services).map((svc) -> [ svc.imageName, svc.config.image ]) _(app.services).map((svc) -> [ svc.imageName, svc.config.image ])
.filter((img) -> img[1]?) .filter((img) -> img[1]?)
.value() .value()
availableWithoutIds = _.map(available, (image) -> _.omit(image, [ 'dockerImageId', 'id' ])) availableWithoutIds = _.map(available, (image) -> _.omit(image, [ 'dockerImageId', 'id' ]))
currentImages = _.flatMap(current.local.apps, allImagesForCurrentApp) currentImages = _.flatMap(current.local.apps, allImagesForCurrentApp)

View File

@ -25,7 +25,6 @@ declare interface Application {
// This is a non-exhaustive typing for ApplicationManager to avoid // This is a non-exhaustive typing for ApplicationManager to avoid
// having to recode the entire class (and all requirements in TS). // having to recode the entire class (and all requirements in TS).
export class ApplicationManager extends EventEmitter { export class ApplicationManager extends EventEmitter {
// These probably could be typed, but the types are so messy that we're // These probably could be typed, but the types are so messy that we're
// best just waiting for the relevant module to be recoded in typescript. // best just waiting for the relevant module to be recoded in typescript.
// At least any types we can be sure of then. // At least any types we can be sure of then.
@ -46,12 +45,14 @@ export class ApplicationManager extends EventEmitter {
// TODO: This actually returns an object, but we don't need the values just yet // TODO: This actually returns an object, but we don't need the values just yet
public setTargetVolatileForService(serviceId: number, opts: Options): void; public setTargetVolatileForService(serviceId: number, opts: Options): void;
public executeStepAction(serviceAction: ServiceAction, opts: Options): Promise<void>; public executeStepAction(
serviceAction: ServiceAction,
opts: Options,
): Promise<void>;
public getStatus(): Promise<DeviceApplicationState>; public getStatus(): Promise<DeviceApplicationState>;
public serviceNameFromId(serviceId: number): Promise<string>; public serviceNameFromId(serviceId: number): Promise<string>;
} }
export default ApplicationManager; export default ApplicationManager;

View File

@ -10,15 +10,12 @@ export class InvalidNetworkNameError extends TypedError {
} }
export class ResourceRecreationAttemptError extends TypedError { export class ResourceRecreationAttemptError extends TypedError {
public constructor( public constructor(public resource: string, public name: string) {
public resource: string,
public name: string,
) {
super( super(
`Trying to create ${resource} with name: ${name}, but a ${resource} `+ `Trying to create ${resource} with name: ${name}, but a ${resource} ` +
'with that name and a different configuration already exists', 'with that name and a different configuration already exists',
); );
} }
} }
export class InvalidNetworkConfigurationError extends TypedError { } export class InvalidNetworkConfigurationError extends TypedError {}

View File

@ -18,72 +18,93 @@ export class NetworkManager {
} }
public getAll(): Bluebird<Network[]> { public getAll(): Bluebird<Network[]> {
return this.getWithBothLabels() return this.getWithBothLabels().map((network: { Name: string }) => {
.map((network: { Name: string }) => { return this.docker
return this.docker.getNetwork(network.Name).inspect() .getNetwork(network.Name)
.then((net) => { .inspect()
return Network.fromDockerNetwork({ .then(net => {
return Network.fromDockerNetwork(
{
docker: this.docker, docker: this.docker,
logger: this.logger, logger: this.logger,
}, net); },
}); net,
}); );
});
});
} }
public getAllByAppId(appId: number): Bluebird<Network[]> { public getAllByAppId(appId: number): Bluebird<Network[]> {
return this.getAll() return this.getAll().filter((network: Network) => network.appId === appId);
.filter((network: Network) => network.appId === appId);
} }
public get(network: { name: string, appId: number }): Bluebird<Network> { public get(network: { name: string; appId: number }): Bluebird<Network> {
return Network.fromNameAndAppId({ return Network.fromNameAndAppId(
logger: this.logger, {
docker: this.docker, logger: this.logger,
}, network.name, network.appId); docker: this.docker,
},
network.name,
network.appId,
);
} }
public supervisorNetworkReady(): Bluebird<boolean> { public supervisorNetworkReady(): Bluebird<boolean> {
return Bluebird.resolve(fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`)) return Bluebird.resolve(
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
)
.then(() => { .then(() => {
return this.docker.getNetwork(constants.supervisorNetworkInterface).inspect(); return this.docker
.getNetwork(constants.supervisorNetworkInterface)
.inspect();
}) })
.then((network) => { .then(network => {
return network.Options['com.docker.network.bridge.name'] === return (
constants.supervisorNetworkInterface; network.Options['com.docker.network.bridge.name'] ===
constants.supervisorNetworkInterface
);
}) })
.catchReturn(NotFoundError, false) .catchReturn(NotFoundError, false)
.catchReturn(ENOENT, false); .catchReturn(ENOENT, false);
} }
public ensureSupervisorNetwork(): Bluebird<void> { public ensureSupervisorNetwork(): Bluebird<void> {
const removeIt = () => { const removeIt = () => {
return Bluebird.resolve(this.docker.getNetwork(constants.supervisorNetworkInterface).remove()) return Bluebird.resolve(
.then(() => { this.docker.getNetwork(constants.supervisorNetworkInterface).remove(),
this.docker.getNetwork(constants.supervisorNetworkInterface).inspect(); ).then(() => {
}); this.docker.getNetwork(constants.supervisorNetworkInterface).inspect();
});
}; };
return Bluebird.resolve(this.docker.getNetwork(constants.supervisorNetworkInterface).inspect()) return Bluebird.resolve(
.then((net) => { this.docker.getNetwork(constants.supervisorNetworkInterface).inspect(),
if (net.Options['com.docker.network.bridge.name'] !== constants.supervisorNetworkInterface) { )
.then(net => {
if (
net.Options['com.docker.network.bridge.name'] !==
constants.supervisorNetworkInterface
) {
return removeIt(); return removeIt();
} else { } else {
return Bluebird.resolve( return Bluebird.resolve(
fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`), fs.stat(`/sys/class/net/${constants.supervisorNetworkInterface}`),
) )
.catch(ENOENT, removeIt) .catch(ENOENT, removeIt)
.return(); .return();
} }
}) })
.catch(NotFoundError, () => { .catch(NotFoundError, () => {
console.log(`Creating ${constants.supervisorNetworkInterface} network`); console.log(`Creating ${constants.supervisorNetworkInterface} network`);
return Bluebird.resolve(this.docker.createNetwork({ return Bluebird.resolve(
Name: constants.supervisorNetworkInterface, this.docker.createNetwork({
Options: { Name: constants.supervisorNetworkInterface,
'com.docker.network.bridge.name': constants.supervisorNetworkInterface, Options: {
}, 'com.docker.network.bridge.name':
})); constants.supervisorNetworkInterface,
},
}),
);
}); });
} }
@ -91,12 +112,12 @@ export class NetworkManager {
return Bluebird.join( return Bluebird.join(
this.docker.listNetworks({ this.docker.listNetworks({
filters: { filters: {
label: [ 'io.resin.supervised' ], label: ['io.resin.supervised'],
}, },
}), }),
this.docker.listNetworks({ this.docker.listNetworks({
filters: { filters: {
label: [ 'io.balena.supervised' ], label: ['io.balena.supervised'],
}, },
}), }),
(legacyNetworks, currentNetworks) => { (legacyNetworks, currentNetworks) => {
@ -104,5 +125,4 @@ export class NetworkManager {
}, },
); );
} }
} }

View File

@ -2,10 +2,7 @@ import * as Bluebird from 'bluebird';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Docker = require('../lib/docker-utils'); import Docker = require('../lib/docker-utils');
import { import { InvalidAppIdError, NotFoundError } from '../lib/errors';
InvalidAppIdError,
NotFoundError,
} from '../lib/errors';
import logTypes = require('../lib/log-types'); import logTypes = require('../lib/log-types');
import { checkInt } from '../lib/validation'; import { checkInt } from '../lib/validation';
import { Logger } from '../logger'; import { Logger } from '../logger';
@ -30,7 +27,6 @@ export interface NetworkOptions {
} }
export class Network { export class Network {
public appId: number; public appId: number;
public name: string; public name: string;
public config: NetworkConfig; public config: NetworkConfig;
@ -66,7 +62,7 @@ export class Network {
driver: network.Driver, driver: network.Driver,
ipam: { ipam: {
driver: network.IPAM.Driver, driver: network.IPAM.Driver,
config: _.map(network.IPAM.Config, (conf) => { config: _.map(network.IPAM.Config, conf => {
const newConf: NetworkConfig['ipam']['config'][0] = { const newConf: NetworkConfig['ipam']['config'][0] = {
subnet: conf.Subnet, subnet: conf.Subnet,
gateway: conf.Gateway, gateway: conf.Gateway,
@ -80,11 +76,13 @@ export class Network {
} }
return newConf; return newConf;
}), }),
options: network.IPAM.Options == null ? { } : network.IPAM.Options, options: network.IPAM.Options == null ? {} : network.IPAM.Options,
}, },
enableIPv6: network.EnableIPv6, enableIPv6: network.EnableIPv6,
internal: network.Internal, internal: network.Internal,
labels: _.omit(ComposeUtils.normalizeLabels(network.Labels), [ 'io.balena.supervised' ]), labels: _.omit(ComposeUtils.normalizeLabels(network.Labels), [
'io.balena.supervised',
]),
options: network.Options, options: network.Options,
}; };
@ -119,12 +117,12 @@ export class Network {
ipam: { ipam: {
driver: 'default', driver: 'default',
config: [], config: [],
options: { }, options: {},
}, },
enableIPv6: false, enableIPv6: false,
internal: false, internal: false,
labels: { }, labels: {},
options: { }, options: {},
}); });
net.config.labels = ComposeUtils.normalizeLabels(net.config.labels); net.config.labels = ComposeUtils.normalizeLabels(net.config.labels);
@ -132,10 +130,12 @@ export class Network {
} }
public create(): Bluebird<void> { public create(): Bluebird<void> {
this.logger.logSystemEvent(logTypes.createNetwork, { network: { name: this.name } }); this.logger.logSystemEvent(logTypes.createNetwork, {
network: { name: this.name },
});
return Network.fromNameAndAppId(this.networkOpts, this.name, this.appId) return Network.fromNameAndAppId(this.networkOpts, this.name, this.appId)
.then((current) => { .then(current => {
if (!this.isEqualConfig(current)) { if (!this.isEqualConfig(current)) {
throw new ResourceRecreationAttemptError('network', this.name); throw new ResourceRecreationAttemptError('network', this.name);
} }
@ -146,7 +146,7 @@ export class Network {
.catch(NotFoundError, () => { .catch(NotFoundError, () => {
return this.docker.createNetwork(this.toDockerConfig()); return this.docker.createNetwork(this.toDockerConfig());
}) })
.tapCatch((err) => { .tapCatch(err => {
this.logger.logSystemEvent(logTypes.createNetworkError, { this.logger.logSystemEvent(logTypes.createNetworkError, {
network: { name: this.name, appId: this.appId }, network: { name: this.name, appId: this.appId },
error: err, error: err,
@ -161,7 +161,7 @@ export class Network {
CheckDuplicate: true, CheckDuplicate: true,
IPAM: { IPAM: {
Driver: this.config.ipam.driver, Driver: this.config.ipam.driver,
Config: _.map(this.config.ipam.config, (conf) => { Config: _.map(this.config.ipam.config, conf => {
const ipamConf: DockerIPAMConfig = { const ipamConf: DockerIPAMConfig = {
Subnet: conf.subnet, Subnet: conf.subnet,
Gateway: conf.gateway, Gateway: conf.gateway,
@ -178,30 +178,32 @@ export class Network {
}, },
EnableIPv6: this.config.enableIPv6, EnableIPv6: this.config.enableIPv6,
Internal: this.config.internal, Internal: this.config.internal,
Labels: _.merge({}, { Labels: _.merge(
'io.balena.supervised': 'true', {},
}, this.config.labels), {
'io.balena.supervised': 'true',
},
this.config.labels,
),
}; };
} }
public remove(): Bluebird<void> { public remove(): Bluebird<void> {
this.logger.logSystemEvent( this.logger.logSystemEvent(logTypes.removeNetwork, {
logTypes.removeNetwork, network: { name: this.name, appId: this.appId },
{ network: { name: this.name, appId: this.appId } }, });
);
return Bluebird.resolve(this.docker.getNetwork(this.getDockerName()).remove()) return Bluebird.resolve(
.tapCatch((error) => { this.docker.getNetwork(this.getDockerName()).remove(),
this.logger.logSystemEvent( ).tapCatch(error => {
logTypes.createNetworkError, this.logger.logSystemEvent(logTypes.createNetworkError, {
{ network: { name: this.name, appId: this.appId }, error }, network: { name: this.name, appId: this.appId },
); error,
}); });
});
} }
public isEqualConfig(network: Network): boolean { public isEqualConfig(network: Network): boolean {
// don't compare the ipam.config if it's not present // don't compare the ipam.config if it's not present
// in the target state (as it will be present in the // in the target state (as it will be present in the
// current state, due to docker populating it with // current state, due to docker populating it with
@ -221,15 +223,12 @@ export class Network {
private static validateComposeConfig(config: NetworkConfig): void { private static validateComposeConfig(config: NetworkConfig): void {
// Check if every ipam config entry has both a subnet and a gateway // Check if every ipam config entry has both a subnet and a gateway
_.each( _.each(_.get(config, 'config.ipam.config', []), ({ subnet, gateway }) => {
_.get(config, 'config.ipam.config', []), if (subnet == null || gateway == null) {
({ subnet, gateway }) => { throw new InvalidNetworkConfigurationError(
if (subnet == null || gateway == null) { 'Network IPAM config entries must have both a subnet and gateway',
throw new InvalidNetworkConfigurationError( );
'Network IPAM config entries must have both a subnet and gateway', }
); });
}
},
);
} }
} }

View File

@ -2,16 +2,15 @@ import * as _ from 'lodash';
import TypedError = require('typed-error'); import TypedError = require('typed-error');
// Adapted from https://github.com/docker/docker-py/blob/master/docker/utils/ports.py#L3 // Adapted from https://github.com/docker/docker-py/blob/master/docker/utils/ports.py#L3
const PORTS_REGEX = const PORTS_REGEX = /^(?:(?:([a-fA-F\d.:]+):)?([\d]*)(?:-([\d]+))?:)?([\d]+)(?:-([\d]+))?(?:\/(udp|tcp))?$/;
/^(?:(?:([a-fA-F\d.:]+):)?([\d]*)(?:-([\d]+))?:)?([\d]+)(?:-([\d]+))?(?:\/(udp|tcp))?$/;
// A regex to extract the protocol and internal port of the incoming Docker options // A regex to extract the protocol and internal port of the incoming Docker options
const DOCKER_OPTS_PORTS_REGEX = /(\d+)(?:\/?([a-z]+))?/i; const DOCKER_OPTS_PORTS_REGEX = /(\d+)(?:\/?([a-z]+))?/i;
export class InvalidPortDefinition extends TypedError { } export class InvalidPortDefinition extends TypedError {}
export interface PortBindings { export interface PortBindings {
[key: string]: Array<{ HostIp: string, HostPort: string }>; [key: string]: Array<{ HostIp: string; HostPort: string }>;
} }
export interface DockerPortOptions { export interface DockerPortOptions {
@ -29,7 +28,6 @@ interface PortRange {
} }
export class PortMap { export class PortMap {
private ports: PortRange; private ports: PortRange;
public constructor(portStrOrObj: string | PortRange) { public constructor(portStrOrObj: string | PortRange) {
@ -41,8 +39,14 @@ export class PortMap {
} }
public toDockerOpts(): DockerPortOptions { public toDockerOpts(): DockerPortOptions {
const internalRange = this.generatePortRange(this.ports.internalStart, this.ports.internalEnd); const internalRange = this.generatePortRange(
const externalRange = this.generatePortRange(this.ports.externalStart, this.ports.externalEnd); this.ports.internalStart,
this.ports.internalEnd,
);
const externalRange = this.generatePortRange(
this.ports.externalStart,
this.ports.externalEnd,
);
const exposedPorts: { [key: string]: {} } = {}; const exposedPorts: { [key: string]: {} } = {};
const portBindings: PortBindings = {}; const portBindings: PortBindings = {};
@ -62,8 +66,11 @@ export class PortMap {
} }
public toExposedPortArray(): string[] { public toExposedPortArray(): string[] {
const internalRange = this.generatePortRange(this.ports.internalStart, this.ports.internalEnd); const internalRange = this.generatePortRange(
return _.map(internalRange, (internal) => { this.ports.internalStart,
this.ports.internalEnd,
);
return _.map(internalRange, internal => {
return `${internal}/${this.ports.protocol}`; return `${internal}/${this.ports.protocol}`;
}); });
} }
@ -80,14 +87,10 @@ export class PortMap {
* and produces a list of PortMap objects, which can then be compared. * and produces a list of PortMap objects, which can then be compared.
* *
*/ */
public static fromDockerOpts( public static fromDockerOpts(portBindings: PortBindings): PortMap[] {
portBindings: PortBindings,
): PortMap[] {
// Create a list of portBindings, rather than the map (which we can't // Create a list of portBindings, rather than the map (which we can't
// order) // order)
const portMaps = _.map(portBindings, (hostObj, internalStr) => { const portMaps = _.map(portBindings, (hostObj, internalStr) => {
const match = internalStr.match(DOCKER_OPTS_PORTS_REGEX); const match = internalStr.match(DOCKER_OPTS_PORTS_REGEX);
if (match == null) { if (match == null) {
throw new Error(`Could not parse docker port output: ${internalStr}`); throw new Error(`Could not parse docker port output: ${internalStr}`);
@ -114,13 +117,14 @@ export class PortMap {
public static normalisePortMaps(portMaps: PortMap[]): PortMap[] { public static normalisePortMaps(portMaps: PortMap[]): PortMap[] {
// Fold any ranges into each other if possible // Fold any ranges into each other if possible
return _(portMaps) return _(portMaps)
.sortBy((p) => p.ports.protocol) .sortBy(p => p.ports.protocol)
.sortBy((p) => p.ports.host) .sortBy(p => p.ports.host)
.sortBy((p) => p.ports.internalStart) .sortBy(p => p.ports.internalStart)
.reduce((res: PortMap[], p: PortMap) => { .reduce((res: PortMap[], p: PortMap) => {
const last = _.last(res); const last = _.last(res);
if (last != null && if (
last != null &&
last.ports.internalEnd + 1 === p.ports.internalStart && last.ports.internalEnd + 1 === p.ports.internalStart &&
last.ports.externalEnd + 1 === p.ports.externalStart && last.ports.externalEnd + 1 === p.ports.externalStart &&
last.ports.protocol === p.ports.protocol && last.ports.protocol === p.ports.protocol &&
@ -138,7 +142,9 @@ export class PortMap {
private parsePortString(portStr: string): void { private parsePortString(portStr: string): void {
const match = portStr.match(PORTS_REGEX); const match = portStr.match(PORTS_REGEX);
if (match == null) { if (match == null) {
throw new InvalidPortDefinition(`Could not parse port definition: ${portStr}`); throw new InvalidPortDefinition(
`Could not parse port definition: ${portStr}`,
);
} }
// Ignore the first parameter (the complete match) and separate the matched // Ignore the first parameter (the complete match) and separate the matched
@ -178,7 +184,10 @@ export class PortMap {
}; };
// Ensure we have the same range // Ensure we have the same range
if (this.ports.internalEnd - this.ports.internalStart !== this.ports.externalEnd - this.ports.externalStart) { if (
this.ports.internalEnd - this.ports.internalStart !==
this.ports.externalEnd - this.ports.externalStart
) {
throw new InvalidPortDefinition( throw new InvalidPortDefinition(
`Range for internal and external ports does not match: ${portStr}`, `Range for internal and external ports does not match: ${portStr}`,
); );
@ -187,7 +196,9 @@ export class PortMap {
private generatePortRange(start: number, end: number): number[] { private generatePortRange(start: number, end: number): number[] {
if (start > end) { if (start > end) {
throw new Error('Incorrect port range! The end port cannot be larger than the start port!'); throw new Error(
'Incorrect port range! The end port cannot be larger than the start port!',
);
} }
return _.range(start, end + 1); return _.range(start, end + 1);

View File

@ -71,7 +71,11 @@ export function sanitiseComposeConfig(
}) as ServiceComposeConfig; }) as ServiceComposeConfig;
if (filtered.length > 0) { if (filtered.length > 0) {
console.log(`Warning: Ignoring unsupported or unknown compose fields: ${filtered.join(', ')}`); console.log(
`Warning: Ignoring unsupported or unknown compose fields: ${filtered.join(
', ',
)}`,
);
} }
return toReturn; return toReturn;

View File

@ -239,9 +239,9 @@ module.exports = class ServiceManager extends EventEmitter
wait = -> wait = ->
Promise.any _.map handoverCompletePaths, (file) -> Promise.any _.map handoverCompletePaths, (file) ->
fs.statAsync(file) fs.statAsync(file)
.then -> .then ->
fs.unlinkAsync(file).catch(_.noop) fs.unlinkAsync(file).catch(_.noop)
.catch -> .catch ->
if Date.now() < deadline if Date.now() < deadline
Promise.delay(pollInterval).then(wait) Promise.delay(pollInterval).then(wait)

View File

@ -22,7 +22,6 @@ import { sanitiseComposeConfig } from './sanitise';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
export class Service { export class Service {
public appId: number | null; public appId: number | null;
public imageId: number | null; public imageId: number | null;
public config: ServiceConfig; public config: ServiceConfig;
@ -64,8 +63,7 @@ export class Service {
'cpus', 'cpus',
].concat(Service.configArrayFields); ].concat(Service.configArrayFields);
private constructor() { private constructor() {}
}
// The type here is actually ServiceComposeConfig, except that the // The type here is actually ServiceComposeConfig, except that the
// keys must be camelCase'd first // keys must be camelCase'd first
@ -77,7 +75,9 @@ export class Service {
appConfig = ComposeUtils.camelCaseConfig(appConfig); appConfig = ComposeUtils.camelCaseConfig(appConfig);
const intOrNull = (val: string | number | null | undefined): number | null => { const intOrNull = (
val: string | number | null | undefined,
): number | null => {
return checkInt(val) || null; return checkInt(val) || null;
}; };
@ -112,13 +112,13 @@ export class Service {
// For any types which do not change, we change config[value] directly // For any types which do not change, we change config[value] directly
// First process the networks correctly // First process the networks correctly
let networks: ServiceConfig['networks'] = { }; let networks: ServiceConfig['networks'] = {};
if (_.isArray(config.networks)) { if (_.isArray(config.networks)) {
_.each(config.networks, (name) => { _.each(config.networks, name => {
networks[name] = { }; networks[name] = {};
}); });
} else if(_.isObject(config.networks)) { } else if (_.isObject(config.networks)) {
networks = config.networks || { }; networks = config.networks || {};
} }
// Prefix the network entries with the app id // Prefix the network entries with the app id
networks = _.mapKeys(networks, (_v, k) => `${service.appId}_${k}`); networks = _.mapKeys(networks, (_v, k) => `${service.appId}_${k}`);
@ -127,17 +127,24 @@ export class Service {
// Check for unsupported networkMode entries // Check for unsupported networkMode entries
if (config.networkMode != null) { if (config.networkMode != null) {
if (/service:(\s*)?.+/.test(config.networkMode)) { if (/service:(\s*)?.+/.test(config.networkMode)) {
console.log('Warning: A network_mode referencing a service is not yet supported. Ignoring.'); console.log(
'Warning: A network_mode referencing a service is not yet supported. Ignoring.',
);
delete config.networkMode; delete config.networkMode;
} else if (/container:(\s*)?.+/.test(config.networkMode)) { } else if (/container:(\s*)?.+/.test(config.networkMode)) {
console.log('Warning: A network_mode referencing a container is not supported. Ignoring.'); console.log(
'Warning: A network_mode referencing a container is not supported. Ignoring.',
);
delete config.networkMode; delete config.networkMode;
} }
} }
// memory strings // memory strings
const memLimit = ComposeUtils.parseMemoryNumber(config.memLimit, '0'); const memLimit = ComposeUtils.parseMemoryNumber(config.memLimit, '0');
const memReservation = ComposeUtils.parseMemoryNumber(config.memReservation, '0'); const memReservation = ComposeUtils.parseMemoryNumber(
config.memReservation,
'0',
);
const shmSize = ComposeUtils.parseMemoryNumber(config.shmSize, '64m'); const shmSize = ComposeUtils.parseMemoryNumber(config.shmSize, '64m');
delete config.memLimit; delete config.memLimit;
delete config.memReservation; delete config.memReservation;
@ -151,7 +158,7 @@ export class Service {
delete config.stopGracePeriod; delete config.stopGracePeriod;
// ulimits // ulimits
const ulimits: ServiceConfig['ulimits'] = { }; const ulimits: ServiceConfig['ulimits'] = {};
_.each(config.ulimits, (limit, name) => { _.each(config.ulimits, (limit, name) => {
if (_.isNumber(limit)) { if (_.isNumber(limit)) {
ulimits[name] = { soft: limit, hard: limit }; ulimits[name] = { soft: limit, hard: limit };
@ -163,11 +170,11 @@ export class Service {
// string or array of strings - normalise to an array // string or array of strings - normalise to an array
if (_.isString(config.dns)) { if (_.isString(config.dns)) {
config.dns = [ config.dns ]; config.dns = [config.dns];
} }
if (_.isString(config.dnsSearch)) { if (_.isString(config.dnsSearch)) {
config.dnsSearch = [ config.dnsSearch ]; config.dnsSearch = [config.dnsSearch];
} }
// Assign network_mode to a default value if necessary // Assign network_mode to a default value if necessary
@ -189,25 +196,27 @@ export class Service {
// If we don't have any networks, we need to // If we don't have any networks, we need to
// create the default with some default options // create the default with some default options
networks[config.networkMode] = { networks[config.networkMode] = {
aliases: [ service.serviceName || '' ], aliases: [service.serviceName || ''],
}; };
} }
} }
// Add default environment variables and labels // Add default environment variables and labels
config.environment = Service.extendEnvVars( config.environment = Service.extendEnvVars(
config.environment || { }, config.environment || {},
options, options,
service.appId || 0, service.appId || 0,
service.serviceName || '', service.serviceName || '',
); );
config.labels = ComposeUtils.normalizeLabels(Service.extendLabels( config.labels = ComposeUtils.normalizeLabels(
config.labels || { }, Service.extendLabels(
options, config.labels || {},
service.appId || 0, options,
service.serviceId || 0, service.appId || 0,
service.serviceName || '', service.serviceId || 0,
)); service.serviceName || '',
),
);
// Any other special case handling // Any other special case handling
if (config.networkMode === 'host' && !config.hostname) { if (config.networkMode === 'host' && !config.hostname) {
@ -215,12 +224,24 @@ export class Service {
} }
config.restart = ComposeUtils.createRestartPolicy(config.restart); config.restart = ComposeUtils.createRestartPolicy(config.restart);
config.command = ComposeUtils.getCommand(config.command, options.imageInfo); config.command = ComposeUtils.getCommand(config.command, options.imageInfo);
config.entrypoint = ComposeUtils.getEntryPoint(config.entrypoint, options.imageInfo); config.entrypoint = ComposeUtils.getEntryPoint(
config.stopSignal = ComposeUtils.getStopSignal(config.stopSignal, options.imageInfo); config.entrypoint,
config.workingDir = ComposeUtils.getWorkingDir(config.workingDir, options.imageInfo); options.imageInfo,
);
config.stopSignal = ComposeUtils.getStopSignal(
config.stopSignal,
options.imageInfo,
);
config.workingDir = ComposeUtils.getWorkingDir(
config.workingDir,
options.imageInfo,
);
config.user = ComposeUtils.getUser(config.user, options.imageInfo); config.user = ComposeUtils.getUser(config.user, options.imageInfo);
const healthcheck = ComposeUtils.getHealthcheck(config.healthcheck, options.imageInfo); const healthcheck = ComposeUtils.getHealthcheck(
config.healthcheck,
options.imageInfo,
);
delete config.healthcheck; delete config.healthcheck;
config.volumes = Service.extendAndSanitiseVolumes( config.volumes = Service.extendAndSanitiseVolumes(
@ -232,7 +253,7 @@ export class Service {
let portMaps: PortMap[] = []; let portMaps: PortMap[] = [];
if (config.ports != null) { if (config.ports != null) {
portMaps = _.map(config.ports, (p) => new PortMap(p)); portMaps = _.map(config.ports, p => new PortMap(p));
} }
delete config.ports; delete config.ports;
@ -241,11 +262,17 @@ export class Service {
if (config.expose != null) { if (config.expose != null) {
expose = _.map(config.expose, ComposeUtils.sanitiseExposeFromCompose); expose = _.map(config.expose, ComposeUtils.sanitiseExposeFromCompose);
} }
const imageExposedPorts = _.get(options.imageInfo, 'Config.ExposedPorts', { }); const imageExposedPorts = _.get(
options.imageInfo,
'Config.ExposedPorts',
{},
);
expose = expose.concat(_.keys(imageExposedPorts)); expose = expose.concat(_.keys(imageExposedPorts));
expose = _.uniq(expose); expose = _.uniq(expose);
// Also add any exposed ports which are implied from the portMaps // Also add any exposed ports which are implied from the portMaps
const exposedFromPortMappings = _.flatMap(portMaps, (port) => port.toExposedPortArray()); const exposedFromPortMappings = _.flatMap(portMaps, port =>
port.toExposedPortArray(),
);
expose = expose.concat(exposedFromPortMappings); expose = expose.concat(exposedFromPortMappings);
delete config.expose; delete config.expose;
@ -263,11 +290,11 @@ export class Service {
} }
if (_.isArray(config.sysctls)) { if (_.isArray(config.sysctls)) {
config.sysctls = _.fromPairs(_.map(config.sysctls, (v) => _.split(v, '='))); config.sysctls = _.fromPairs(_.map(config.sysctls, v => _.split(v, '=')));
} }
config.sysctls = _.mapValues(config.sysctls, String); config.sysctls = _.mapValues(config.sysctls, String);
_.each([ 'cpuShares', 'cpuQuota', 'oomScoreAdj' ], (key)=> { _.each(['cpuShares', 'cpuQuota', 'oomScoreAdj'], key => {
const numVal = checkInt(config[key]); const numVal = checkInt(config[key]);
if (numVal) { if (numVal) {
config[key] = numVal; config[key] = numVal;
@ -288,7 +315,7 @@ export class Service {
let tmpfs: string[] = []; let tmpfs: string[] = [];
if (config.tmpfs != null) { if (config.tmpfs != null) {
if (_.isString(config.tmpfs)) { if (_.isString(config.tmpfs)) {
tmpfs = [ config.tmpfs ]; tmpfs = [config.tmpfs];
} else { } else {
tmpfs = config.tmpfs; tmpfs = config.tmpfs;
} }
@ -300,33 +327,33 @@ export class Service {
service.config = _.defaults(config, { service.config = _.defaults(config, {
portMaps, portMaps,
capAdd: [ ], capAdd: [],
capDrop:[ ], capDrop: [],
command: [ ], command: [],
cgroupParent: '', cgroupParent: '',
devices, devices,
dnsOpt: [ ], dnsOpt: [],
entrypoint: '', entrypoint: '',
extraHosts: [ ], extraHosts: [],
expose, expose,
networks, networks,
dns: [ ], dns: [],
dnsSearch: [ ], dnsSearch: [],
environment: { }, environment: {},
labels: { }, labels: {},
networkMode: '', networkMode: '',
ulimits, ulimits,
groupAdd: [ ], groupAdd: [],
healthcheck, healthcheck,
pid: '', pid: '',
pidsLimit: 0, pidsLimit: 0,
securityOpt: [ ], securityOpt: [],
stopGracePeriod, stopGracePeriod,
stopSignal: '', stopSignal: '',
sysctls: { }, sysctls: {},
tmpfs, tmpfs,
usernsMode: '', usernsMode: '',
volumes: [ ], volumes: [],
restart: 'always', restart: 'always',
cpuShares: 0, cpuShares: 0,
cpuQuota: 0, cpuQuota: 0,
@ -354,14 +381,16 @@ export class Service {
return service; return service;
} }
public static fromDockerContainer(container: Dockerode.ContainerInspectInfo): Service { public static fromDockerContainer(
container: Dockerode.ContainerInspectInfo,
): Service {
const svc = new Service(); const svc = new Service();
if (container.State.Running) { if (container.State.Running) {
svc.status = 'Running'; svc.status = 'Running';
} else if(container.State.Status === 'created') { } else if (container.State.Status === 'created') {
svc.status = 'Installed'; svc.status = 'Installed';
} else if(container.State.Status === 'dead') { } else if (container.State.Status === 'dead') {
svc.status = 'Dead'; svc.status = 'Dead';
} else { } else {
svc.status = container.State.Status; svc.status = container.State.Status;
@ -377,23 +406,27 @@ export class Service {
hostname = ''; hostname = '';
} }
let networks: ServiceConfig['networks'] = { }; let networks: ServiceConfig['networks'] = {};
if (_.get(container, 'NetworkSettings.Networks', null) != null) { if (_.get(container, 'NetworkSettings.Networks', null) != null) {
networks = ComposeUtils.dockerNetworkToServiceNetwork(container.NetworkSettings.Networks); networks = ComposeUtils.dockerNetworkToServiceNetwork(
container.NetworkSettings.Networks,
);
} }
const ulimits: ServiceConfig['ulimits'] = { }; const ulimits: ServiceConfig['ulimits'] = {};
_.each(container.HostConfig.Ulimits, ({ Name, Soft, Hard }) => { _.each(container.HostConfig.Ulimits, ({ Name, Soft, Hard }) => {
ulimits[Name] = { soft: Soft, hard: Hard }; ulimits[Name] = { soft: Soft, hard: Hard };
}); });
const portMaps = PortMap.fromDockerOpts(container.HostConfig.PortBindings); const portMaps = PortMap.fromDockerOpts(container.HostConfig.PortBindings);
let expose = _.flatMap( let expose = _.flatMap(
_.flatMap(portMaps, (p) => p.toDockerOpts().exposedPorts), _.flatMap(portMaps, p => p.toDockerOpts().exposedPorts),
_.keys, _.keys,
); );
if (container.Config.ExposedPorts != null) { if (container.Config.ExposedPorts != null) {
expose = expose.concat(_.map(container.Config.ExposedPorts, (_v, k) => k.toString())); expose = expose.concat(
_.map(container.Config.ExposedPorts, (_v, k) => k.toString()),
);
} }
expose = _.uniq(expose); expose = _.uniq(expose);
@ -425,21 +458,24 @@ export class Service {
hostname, hostname,
command: container.Config.Cmd || '', command: container.Config.Cmd || '',
entrypoint: container.Config.Entrypoint || '', entrypoint: container.Config.Entrypoint || '',
volumes: _.concat(container.HostConfig.Binds || [], _.keys(container.Config.Volumes || { })), volumes: _.concat(
container.HostConfig.Binds || [],
_.keys(container.Config.Volumes || {}),
),
image: container.Config.Image, image: container.Config.Image,
environment: _.omit(conversions.envArrayToObject(container.Config.Env || [ ]), [ environment: _.omit(
'RESIN_DEVICE_NAME_AT_INIT', conversions.envArrayToObject(container.Config.Env || []),
'BALENA_DEVICE_NAME_AT_INIT', ['RESIN_DEVICE_NAME_AT_INIT', 'BALENA_DEVICE_NAME_AT_INIT'],
]), ),
privileged: container.HostConfig.Privileged || false, privileged: container.HostConfig.Privileged || false,
labels: ComposeUtils.normalizeLabels(container.Config.Labels || { }), labels: ComposeUtils.normalizeLabels(container.Config.Labels || {}),
running: container.State.Running, running: container.State.Running,
restart, restart,
capAdd: container.HostConfig.CapAdd || [ ], capAdd: container.HostConfig.CapAdd || [],
capDrop: container.HostConfig.CapDrop || [ ], capDrop: container.HostConfig.CapDrop || [],
devices: container.HostConfig.Devices || [ ], devices: container.HostConfig.Devices || [],
networks, networks,
memLimit: container.HostConfig.Memory || 0 , memLimit: container.HostConfig.Memory || 0,
memReservation: container.HostConfig.MemoryReservation || 0, memReservation: container.HostConfig.MemoryReservation || 0,
shmSize: container.HostConfig.ShmSize || 0, shmSize: container.HostConfig.ShmSize || 0,
cpuShares: container.HostConfig.CpuShares || 0, cpuShares: container.HostConfig.CpuShares || 0,
@ -450,24 +486,24 @@ export class Service {
domainname: container.Config.Domainname || '', domainname: container.Config.Domainname || '',
oomKillDisable: container.HostConfig.OomKillDisable || false, oomKillDisable: container.HostConfig.OomKillDisable || false,
oomScoreAdj: container.HostConfig.OomScoreAdj || 0, oomScoreAdj: container.HostConfig.OomScoreAdj || 0,
dns: container.HostConfig.Dns || [ ], dns: container.HostConfig.Dns || [],
dnsSearch: container.HostConfig.DnsSearch || [ ], dnsSearch: container.HostConfig.DnsSearch || [],
dnsOpt: container.HostConfig.DnsOptions || [ ], dnsOpt: container.HostConfig.DnsOptions || [],
tmpfs, tmpfs,
extraHosts: container.HostConfig.ExtraHosts || [ ], extraHosts: container.HostConfig.ExtraHosts || [],
ulimits, ulimits,
stopSignal: (container.Config as any).StopSignal || '', stopSignal: (container.Config as any).StopSignal || '',
stopGracePeriod: (container.Config as any).StopTimeout || 0, stopGracePeriod: (container.Config as any).StopTimeout || 0,
healthcheck: ComposeUtils.dockerHealthcheckToServiceHealthcheck( healthcheck: ComposeUtils.dockerHealthcheckToServiceHealthcheck(
(container.Config as any).Healthcheck || { }, (container.Config as any).Healthcheck || {},
), ),
readOnly: container.HostConfig.ReadonlyRootfs || false, readOnly: container.HostConfig.ReadonlyRootfs || false,
sysctls: container.HostConfig.Sysctls || { }, sysctls: container.HostConfig.Sysctls || {},
cgroupParent: container.HostConfig.CgroupParent || '', cgroupParent: container.HostConfig.CgroupParent || '',
groupAdd: container.HostConfig.GroupAdd || [ ], groupAdd: container.HostConfig.GroupAdd || [],
pid: container.HostConfig.PidMode || '', pid: container.HostConfig.PidMode || '',
pidsLimit: container.HostConfig.PidsLimit || 0, pidsLimit: container.HostConfig.PidsLimit || 0,
securityOpt: container.HostConfig.SecurityOpt || [ ], securityOpt: container.HostConfig.SecurityOpt || [],
usernsMode: container.HostConfig.UsernsMode || '', usernsMode: container.HostConfig.UsernsMode || '',
ipc: container.HostConfig.IpcMode || '', ipc: container.HostConfig.IpcMode || '',
macAddress: (container.Config as any).MacAddress || '', macAddress: (container.Config as any).MacAddress || '',
@ -488,12 +524,14 @@ export class Service {
return svc; return svc;
} }
public toDockerContainer(opts: { deviceName: string }): Dockerode.ContainerCreateOptions { public toDockerContainer(opts: {
deviceName: string;
}): Dockerode.ContainerCreateOptions {
const { binds, volumes } = this.getBindsAndVolumes(); const { binds, volumes } = this.getBindsAndVolumes();
const { exposedPorts, portBindings } = this.generateExposeAndPorts(); const { exposedPorts, portBindings } = this.generateExposeAndPorts();
const tmpFs: Dictionary<''> = { }; const tmpFs: Dictionary<''> = {};
_.each(this.config.tmpfs, (tmp) => { _.each(this.config.tmpfs, tmp => {
tmpFs[tmp] = ''; tmpFs[tmp] = '';
}); });
@ -509,14 +547,21 @@ export class Service {
Volumes: volumes, Volumes: volumes,
// Typings are wrong here, the docker daemon accepts a string or string[], // Typings are wrong here, the docker daemon accepts a string or string[],
Entrypoint: this.config.entrypoint as string, Entrypoint: this.config.entrypoint as string,
Env: conversions.envObjectToArray(_.assign({ Env: conversions.envObjectToArray(
RESIN_DEVICE_NAME_AT_INIT: opts.deviceName, _.assign(
BALENA_DEVICE_NAME_AT_INIT: opts.deviceName, {
}, this.config.environment)), RESIN_DEVICE_NAME_AT_INIT: opts.deviceName,
BALENA_DEVICE_NAME_AT_INIT: opts.deviceName,
},
this.config.environment,
),
),
ExposedPorts: exposedPorts, ExposedPorts: exposedPorts,
Image: this.config.image, Image: this.config.image,
Labels: this.config.labels, Labels: this.config.labels,
NetworkingConfig: ComposeUtils.serviceNetworksToDockerNetworks(mainNetwork), NetworkingConfig: ComposeUtils.serviceNetworksToDockerNetworks(
mainNetwork,
),
StopSignal: this.config.stopSignal, StopSignal: this.config.stopSignal,
Domainname: this.config.domainname, Domainname: this.config.domainname,
Hostname: this.config.hostname, Hostname: this.config.hostname,
@ -542,8 +587,12 @@ export class Service {
PidsLimit: this.config.pidsLimit, PidsLimit: this.config.pidsLimit,
SecurityOpt: this.config.securityOpt, SecurityOpt: this.config.securityOpt,
Sysctls: this.config.sysctls, Sysctls: this.config.sysctls,
Ulimits: ComposeUtils.serviceUlimitsToDockerUlimits(this.config.ulimits), Ulimits: ComposeUtils.serviceUlimitsToDockerUlimits(
RestartPolicy: ComposeUtils.serviceRestartToDockerRestartPolicy(this.config.restart), this.config.ulimits,
),
RestartPolicy: ComposeUtils.serviceRestartToDockerRestartPolicy(
this.config.restart,
),
CpuShares: this.config.cpuShares, CpuShares: this.config.cpuShares,
CpuQuota: this.config.cpuQuota, CpuQuota: this.config.cpuQuota,
// Type missing, and HostConfig isn't defined as a seperate object // Type missing, and HostConfig isn't defined as a seperate object
@ -561,7 +610,9 @@ export class Service {
NanoCpus: this.config.cpus, NanoCpus: this.config.cpus,
IpcMode: this.config.ipc, IpcMode: this.config.ipc,
} as Dockerode.ContainerCreateOptions['HostConfig'], } as Dockerode.ContainerCreateOptions['HostConfig'],
Healthcheck: ComposeUtils.serviceHealthcheckToDockerHealthcheck(this.config.healthcheck), Healthcheck: ComposeUtils.serviceHealthcheckToDockerHealthcheck(
this.config.healthcheck,
),
StopTimeout: this.config.stopGracePeriod, StopTimeout: this.config.stopGracePeriod,
}; };
} }
@ -574,47 +625,54 @@ export class Service {
sameNetworks = false; sameNetworks = false;
return; return;
} }
sameNetworks = sameNetworks && this.isSameNetwork(this.config.networks[name], network); sameNetworks =
sameNetworks && this.isSameNetwork(this.config.networks[name], network);
}); });
// Check the configuration for any changes // Check the configuration for any changes
const thisOmitted = _.omit(this.config, Service.omitFields); const thisOmitted = _.omit(this.config, Service.omitFields);
const otherOmitted = _.omit(service.config, Service.omitFields); const otherOmitted = _.omit(service.config, Service.omitFields);
let sameConfig = _.isEqual( let sameConfig = _.isEqual(thisOmitted, otherOmitted);
thisOmitted,
otherOmitted,
);
const nonArrayEquals = sameConfig; const nonArrayEquals = sameConfig;
// Check for array fields which don't match // Check for array fields which don't match
const differentArrayFields: string[] = []; const differentArrayFields: string[] = [];
sameConfig = sameConfig && _.every(Service.configArrayFields, (field: ServiceConfigArrayField) => { sameConfig =
return _.isEmpty( sameConfig &&
_.xorWith( _.every(Service.configArrayFields, (field: ServiceConfigArrayField) => {
// TODO: The typings here aren't accepted, even though we return _.isEmpty(
// know it's fine _.xorWith(
(this.config as any)[field], // TODO: The typings here aren't accepted, even though we
(service.config as any)[field], // know it's fine
(a, b) => { (this.config as any)[field],
const eq = _.isEqual(a, b); (service.config as any)[field],
if (!eq) { (a, b) => {
differentArrayFields.push(field); const eq = _.isEqual(a, b);
} if (!eq) {
return eq; differentArrayFields.push(field);
}, }
), return eq;
); },
}); ),
);
});
if (!(sameConfig && sameNetworks)) { if (!(sameConfig && sameNetworks)) {
// Add some console output for why a service is not matching // Add some console output for why a service is not matching
// so that if we end up in a restart loop, we know exactly why // so that if we end up in a restart loop, we know exactly why
console.log(`Replacing container for service ${this.serviceName} because of config changes:`); console.log(
`Replacing container for service ${
this.serviceName
} because of config changes:`,
);
if (!nonArrayEquals) { if (!nonArrayEquals) {
// Try not to leak any sensitive information // Try not to leak any sensitive information
const diffObj = diff(thisOmitted, otherOmitted) as ServiceConfig; const diffObj = diff(thisOmitted, otherOmitted) as ServiceConfig;
if (diffObj.environment != null) { if (diffObj.environment != null) {
diffObj.environment = _.mapValues(diffObj.environment, () => 'hidden'); diffObj.environment = _.mapValues(
diffObj.environment,
() => 'hidden',
);
} }
console.log(' Non-array fields: ', JSON.stringify(diffObj)); console.log(' Non-array fields: ', JSON.stringify(diffObj));
} }
@ -625,7 +683,6 @@ export class Service {
if (!sameNetworks) { if (!sameNetworks) {
console.log(' Network changes detected'); console.log(' Network changes detected');
} }
} }
return sameNetworks && sameConfig; return sameNetworks && sameConfig;
} }
@ -635,19 +692,26 @@ export class Service {
} }
public isEqualExceptForRunningState(service: Service): boolean { public isEqualExceptForRunningState(service: Service): boolean {
return this.isEqualConfig(service) && return (
this.isEqualConfig(service) &&
this.releaseId === service.releaseId && this.releaseId === service.releaseId &&
this.imageId === service.imageId; this.imageId === service.imageId
);
} }
public isEqual(service: Service): boolean { public isEqual(service: Service): boolean {
return this.isEqualExceptForRunningState(service) && return (
this.config.running === service.config.running; this.isEqualExceptForRunningState(service) &&
this.config.running === service.config.running
);
} }
public getNamedVolumes() { public getNamedVolumes() {
const defaults = Service.defaultBinds(this.appId || 0, this.serviceName || ''); const defaults = Service.defaultBinds(
const validVolumes = _.map(this.config.volumes, (volume) => { this.appId || 0,
this.serviceName || '',
);
const validVolumes = _.map(this.config.volumes, volume => {
if (_.includes(defaults, volume) || !_.includes(volume, ':')) { if (_.includes(defaults, volume) || !_.includes(volume, ':')) {
return null; return null;
} }
@ -655,7 +719,9 @@ export class Service {
if (!path.isAbsolute(bindSource)) { if (!path.isAbsolute(bindSource)) {
const match = bindSource.match(/[0-9]+_(.+)/); const match = bindSource.match(/[0-9]+_(.+)/);
if (match == null) { if (match == null) {
console.log('Error: There was an error parsing a volume bind source, ignoring.'); console.log(
'Error: There was an error parsing a volume bind source, ignoring.',
);
console.log(' bind source: ', bindSource); console.log(' bind source: ', bindSource);
return null; return null;
} }
@ -675,20 +741,23 @@ export class Service {
} }
private handoverCompletePathOnHost(): string { private handoverCompletePathOnHost(): string {
return path.join(constants.rootMountPoint, updateLock.lockPath(this.appId || 0, this.serviceName || '')); return path.join(
constants.rootMountPoint,
updateLock.lockPath(this.appId || 0, this.serviceName || ''),
);
} }
private getBindsAndVolumes(): { private getBindsAndVolumes(): {
binds: string[], binds: string[];
volumes: { [volName: string]: { } } volumes: { [volName: string]: {} };
} { } {
const binds: string[] = [ ]; const binds: string[] = [];
const volumes: { [volName: string]: { } } = { }; const volumes: { [volName: string]: {} } = {};
_.each(this.config.volumes, (volume) => { _.each(this.config.volumes, volume => {
if (_.includes(volume, ':')) { if (_.includes(volume, ':')) {
binds.push(volume); binds.push(volume);
} else { } else {
volumes[volume] = { }; volumes[volume] = {};
} }
}); });
@ -696,20 +765,20 @@ export class Service {
} }
private generateExposeAndPorts(): DockerPortOptions { private generateExposeAndPorts(): DockerPortOptions {
const exposed: DockerPortOptions['exposedPorts'] = { }; const exposed: DockerPortOptions['exposedPorts'] = {};
const ports: DockerPortOptions['portBindings'] = { }; const ports: DockerPortOptions['portBindings'] = {};
_.each(this.config.portMaps, (pmap) => { _.each(this.config.portMaps, pmap => {
const { exposedPorts, portBindings } = pmap.toDockerOpts(); const { exposedPorts, portBindings } = pmap.toDockerOpts();
_.merge(exposed, exposedPorts); _.merge(exposed, exposedPorts);
_.merge(ports, portBindings); _.merge(ports, portBindings);
}); });
// We also want to merge the compose and image exposedPorts // We also want to merge the compose and image exposedPorts
// into the list of exposedPorts // into the list of exposedPorts
const composeExposed: DockerPortOptions['exposedPorts'] = { }; const composeExposed: DockerPortOptions['exposedPorts'] = {};
_.each(this.config.expose, (port) => { _.each(this.config.expose, port => {
composeExposed[port] = { }; composeExposed[port] = {};
}); });
_.merge(exposed, composeExposed); _.merge(exposed, composeExposed);
@ -722,22 +791,29 @@ export class Service {
appId: number, appId: number,
serviceName: string, serviceName: string,
): { [envVarName: string]: string } { ): { [envVarName: string]: string } {
let defaultEnv: { [ envVarName: string]: string } = {}; let defaultEnv: { [envVarName: string]: string } = {};
for(let namespace of [ 'BALENA', 'RESIN' ]){ for (let namespace of ['BALENA', 'RESIN']) {
_.assign(defaultEnv, _.mapKeys({ _.assign(
APP_ID: appId.toString(), defaultEnv,
APP_NAME: options.appName, _.mapKeys(
SERVICE_NAME: serviceName, {
DEVICE_UUID: options.uuid, APP_ID: appId.toString(),
DEVICE_TYPE: options.deviceType, APP_NAME: options.appName,
HOST_OS_VERSION: options.osVersion, SERVICE_NAME: serviceName,
SUPERVISOR_VERSION: options.version, DEVICE_UUID: options.uuid,
APP_LOCK_PATH: '/tmp/balena/updates.lock', DEVICE_TYPE: options.deviceType,
}, (_val, key) => `${namespace}_${key}`)); HOST_OS_VERSION: options.osVersion,
SUPERVISOR_VERSION: options.version,
APP_LOCK_PATH: '/tmp/balena/updates.lock',
},
(_val, key) => `${namespace}_${key}`,
),
);
defaultEnv[namespace] = '1'; defaultEnv[namespace] = '1';
} }
defaultEnv['RESIN_SERVICE_KILL_ME_PATH'] = '/tmp/balena/handover-complete'; defaultEnv['RESIN_SERVICE_KILL_ME_PATH'] = '/tmp/balena/handover-complete';
defaultEnv['BALENA_SERVICE_HANDOVER_COMPLETE_PATH'] = '/tmp/balena/handover-complete'; defaultEnv['BALENA_SERVICE_HANDOVER_COMPLETE_PATH'] =
'/tmp/balena/handover-complete';
defaultEnv['USER'] = 'root'; defaultEnv['USER'] = 'root';
let env = _.defaults(environment, defaultEnv); let env = _.defaults(environment, defaultEnv);
@ -775,18 +851,23 @@ export class Service {
.isEqual(targetAliases); .isEqual(targetAliases);
} else { } else {
// Otherwise compare them literally // Otherwise compare them literally
sameNetwork = _.isEmpty(_.xorWith(currentAliases, targetAliases, _.isEqual)); sameNetwork = _.isEmpty(
_.xorWith(currentAliases, targetAliases, _.isEqual),
);
} }
} }
} }
if (target.ipv4Address != null) { if (target.ipv4Address != null) {
sameNetwork = sameNetwork && _.isEqual(current.ipv4Address, target.ipv4Address); sameNetwork =
sameNetwork && _.isEqual(current.ipv4Address, target.ipv4Address);
} }
if (target.ipv6Address != null) { if (target.ipv6Address != null) {
sameNetwork = sameNetwork && _.isEqual(current.ipv6Address, target.ipv6Address); sameNetwork =
sameNetwork && _.isEqual(current.ipv6Address, target.ipv6Address);
} }
if (target.linkLocalIps != null) { if (target.linkLocalIps != null) {
sameNetwork = sameNetwork && _.isEqual(current.linkLocalIps, target.linkLocalIps); sameNetwork =
sameNetwork && _.isEqual(current.linkLocalIps, target.linkLocalIps);
} }
return sameNetwork; return sameNetwork;
} }
@ -805,7 +886,7 @@ export class Service {
'io.balena.service-name': serviceName, 'io.balena.service-name': serviceName,
}); });
const imageLabels = _.get(imageInfo, 'Config.Labels', { }); const imageLabels = _.get(imageInfo, 'Config.Labels', {});
newLabels = _.defaults(newLabels, imageLabels); newLabels = _.defaults(newLabels, imageLabels);
return newLabels; return newLabels;
} }
@ -818,10 +899,10 @@ export class Service {
): ServiceConfig['volumes'] { ): ServiceConfig['volumes'] {
let volumes: ServiceConfig['volumes'] = []; let volumes: ServiceConfig['volumes'] = [];
_.each(composeVolumes, (volume) => { _.each(composeVolumes, volume => {
const isBind = _.includes(volume, ':'); const isBind = _.includes(volume, ':');
if (isBind) { if (isBind) {
const [ bindSource, bindDest, mode ] = volume.split(':'); const [bindSource, bindDest, mode] = volume.split(':');
if (!path.isAbsolute(bindSource)) { if (!path.isAbsolute(bindSource)) {
// namespace our volumes by appId // namespace our volumes by appId
let volumeDef = `${appId}_${bindSource}:${bindDest}`; let volumeDef = `${appId}_${bindSource}:${bindDest}`;
@ -850,5 +931,4 @@ export class Service {
`${updateLock.lockPath(appId, serviceName)}:/tmp/balena`, `${updateLock.lockPath(appId, serviceName)}:/tmp/balena`,
]; ];
} }
} }

View File

@ -38,7 +38,12 @@ export interface NetworkConfig {
driver: string; driver: string;
ipam: { ipam: {
driver: string; driver: string;
config: Array<{ subnet: string, gateway: string, ipRange?: string, auxAddress?: string }>; config: Array<{
subnet: string;
gateway: string;
ipRange?: string;
auxAddress?: string;
}>;
options: { [optName: string]: string }; options: { [optName: string]: string };
}; };
enableIPv6: boolean; enableIPv6: boolean;

View File

@ -47,14 +47,16 @@ export interface ServiceComposeConfig {
labels?: { [labelName: string]: string }; labels?: { [labelName: string]: string };
running: boolean; running: boolean;
networkMode?: string; networkMode?: string;
networks?: string[] | { networks?:
[networkName: string]: { | string[]
aliases?: string[]; | {
ipv4Address?: string; [networkName: string]: {
ipv6Address?: string; aliases?: string[];
linkLocalIps?: string[]; ipv4Address?: string;
} ipv6Address?: string;
}; linkLocalIps?: string[];
};
};
pid?: string; pid?: string;
pidsLimit?: number; pidsLimit?: number;
ports?: string[]; ports?: string[];
@ -63,7 +65,7 @@ export interface ServiceComposeConfig {
stopSignal?: string; stopSignal?: string;
sysctls?: { [name: string]: string }; sysctls?: { [name: string]: string };
ulimits?: { ulimits?: {
[ulimitName: string]: number | { soft: number, hard: number }; [ulimitName: string]: number | { soft: number; hard: number };
}; };
usernsMode?: string; usernsMode?: string;
volumes?: string[]; volumes?: string[];
@ -118,7 +120,7 @@ export interface ServiceConfig {
ipv4Address?: string; ipv4Address?: string;
ipv6Address?: string; ipv6Address?: string;
linkLocalIps?: string[]; linkLocalIps?: string[];
} };
}; };
pid: string; pid: string;
pidsLimit: number; pidsLimit: number;
@ -127,7 +129,7 @@ export interface ServiceConfig {
stopSignal: string; stopSignal: string;
sysctls: { [name: string]: string }; sysctls: { [name: string]: string };
ulimits: { ulimits: {
[ulimitName: string]: { soft: number, hard: number }; [ulimitName: string]: { soft: number; hard: number };
}; };
usernsMode: string; usernsMode: string;
volumes: string[]; volumes: string[];
@ -152,19 +154,20 @@ export interface ServiceConfig {
tty: boolean; tty: boolean;
} }
export type ServiceConfigArrayField = 'volumes' | export type ServiceConfigArrayField =
'devices' | | 'volumes'
'capAdd' | | 'devices'
'capDrop' | | 'capAdd'
'dns' | | 'capDrop'
'dnsSearch' | | 'dns'
'dnsOpt' | | 'dnsSearch'
'expose' | | 'dnsOpt'
'tmpfs' | | 'expose'
'extraHosts' | | 'tmpfs'
'ulimitsArray' | | 'extraHosts'
'groupAdd' | | 'ulimitsArray'
'securityOpt'; | 'groupAdd'
| 'securityOpt';
// The config directly from the application manager, which contains // The config directly from the application manager, which contains
// application information, plus the compose data // application information, plus the compose data
@ -201,4 +204,3 @@ export interface DockerDevice {
PathInContainer: string; PathInContainer: string;
CgroupPermissions: string; CgroupPermissions: string;
} }

View File

@ -16,7 +16,9 @@ import {
ServiceHealthcheck, ServiceHealthcheck,
} from './types/service'; } from './types/service';
export function camelCaseConfig(literalConfig: ConfigMap): ServiceComposeConfig { export function camelCaseConfig(
literalConfig: ConfigMap,
): ServiceComposeConfig {
const config = _.mapKeys(literalConfig, (_v, k) => _.camelCase(k)); const config = _.mapKeys(literalConfig, (_v, k) => _.camelCase(k));
// Networks can either be an object or array, but given _.isObject // Networks can either be an object or array, but given _.isObject
@ -31,7 +33,10 @@ export function camelCaseConfig(literalConfig: ConfigMap): ServiceComposeConfig
return config as ServiceComposeConfig; return config as ServiceComposeConfig;
} }
export function parseMemoryNumber(valueAsString: string | null | undefined, defaultValue?: string): number { export function parseMemoryNumber(
valueAsString: string | null | undefined,
defaultValue?: string,
): number {
if (valueAsString == null) { if (valueAsString == null) {
if (defaultValue != null) { if (defaultValue != null) {
return parseMemoryNumber(defaultValue); return parseMemoryNumber(defaultValue);
@ -46,7 +51,17 @@ export function parseMemoryNumber(valueAsString: string | null | undefined, defa
return 0; return 0;
} }
const num = match[1]; const num = match[1];
const pow: { [key: string]: number } = { '': 0, b: 0, B: 0, K: 1, k: 1, m: 2, M: 2, g: 3, G: 3 }; const pow: { [key: string]: number } = {
'': 0,
b: 0,
B: 0,
K: 1,
k: 1,
m: 2,
M: 2,
g: 3,
G: 3,
};
return parseInt(num, 10) * 1024 ** pow[match[2]]; return parseInt(num, 10) * 1024 ** pow[match[2]];
} }
@ -57,9 +72,7 @@ export const validRestartPolicies = [
'unless-stopped', 'unless-stopped',
]; ];
export function createRestartPolicy( export function createRestartPolicy(name?: string): string {
name?: string,
): string {
if (name == null) { if (name == null) {
return 'always'; return 'always';
} }
@ -67,12 +80,14 @@ export function createRestartPolicy(
// Ensure that name is a string, otherwise the below could // Ensure that name is a string, otherwise the below could
// throw // throw
if (!_.isString(name)) { if (!_.isString(name)) {
console.log(`Warning: Non-string argument for restart field: ${name} - ignoring.`); console.log(
`Warning: Non-string argument for restart field: ${name} - ignoring.`,
);
return 'always'; return 'always';
} }
name = name.toLowerCase().trim(); name = name.toLowerCase().trim();
if(!_.includes(validRestartPolicies, name)) { if (!_.includes(validRestartPolicies, name)) {
return 'always'; return 'always';
} }
@ -87,7 +102,9 @@ function processCommandString(command: string): string {
return command.replace(/(\$)/g, '\\$1'); return command.replace(/(\$)/g, '\\$1');
} }
function processCommandParsedArrayElement(arg: string | { [key: string]: string}): string { function processCommandParsedArrayElement(
arg: string | { [key: string]: string },
): string {
if (_.isString(arg)) { if (_.isString(arg)) {
return arg; return arg;
} }
@ -150,7 +167,7 @@ export function dockerHealthcheckToServiceHealthcheck(
healthcheck?: Dockerode.DockerHealthcheck, healthcheck?: Dockerode.DockerHealthcheck,
): ServiceHealthcheck { ): ServiceHealthcheck {
if (healthcheck == null || _.isEmpty(healthcheck)) { if (healthcheck == null || _.isEmpty(healthcheck)) {
return { test: [ 'NONE' ] }; return { test: ['NONE'] };
} }
const serviceHC: ServiceHealthcheck = { const serviceHC: ServiceHealthcheck = {
test: healthcheck.Test, test: healthcheck.Test,
@ -175,11 +192,9 @@ export function dockerHealthcheckToServiceHealthcheck(
return serviceHC; return serviceHC;
} }
function buildHealthcheckTest( function buildHealthcheckTest(test: string | string[]): string[] {
test: string | string[],
): string[] {
if (_.isString(test)) { if (_.isString(test)) {
return [ 'CMD-SHELL', test]; return ['CMD-SHELL', test];
} }
return test; return test;
} }
@ -190,14 +205,13 @@ function getNanoseconds(timeStr: string): number {
export function composeHealthcheckToServiceHealthcheck( export function composeHealthcheckToServiceHealthcheck(
healthcheck: ComposeHealthcheck | null | undefined, healthcheck: ComposeHealthcheck | null | undefined,
): ServiceHealthcheck | { } { ): ServiceHealthcheck | {} {
if (healthcheck == null) { if (healthcheck == null) {
return { }; return {};
} }
if (healthcheck.disable) { if (healthcheck.disable) {
return { test: [ 'NONE' ] }; return { test: ['NONE'] };
} }
const serviceHC: ServiceHealthcheck = { const serviceHC: ServiceHealthcheck = {
@ -236,7 +250,11 @@ export function getHealthcheck(
); );
// Overlay any compose healthcheck fields on the image healthchecks // Overlay any compose healthcheck fields on the image healthchecks
return _.assign({ test: [ 'NONE' ] }, imageServiceHealthcheck, composeServiceHealthcheck); return _.assign(
{ test: ['NONE'] },
imageServiceHealthcheck,
composeServiceHealthcheck,
);
} }
export function serviceHealthcheckToDockerHealthcheck( export function serviceHealthcheckToDockerHealthcheck(
@ -255,8 +273,10 @@ export function getWorkingDir(
workingDir: string | null | undefined, workingDir: string | null | undefined,
imageInfo?: Dockerode.ImageInspectInfo, imageInfo?: Dockerode.ImageInspectInfo,
): string { ): string {
return (workingDir != null ? workingDir : _.get(imageInfo, 'Config.WorkingDir', '')) return (workingDir != null
.replace(/(^.+)\/$/, '$1'); ? workingDir
: _.get(imageInfo, 'Config.WorkingDir', '')
).replace(/(^.+)\/$/, '$1');
} }
export function getUser( export function getUser(
@ -266,20 +286,16 @@ export function getUser(
return user != null ? user : _.get(imageInfo, 'Config.User', ''); return user != null ? user : _.get(imageInfo, 'Config.User', '');
} }
export function sanitiseExposeFromCompose( export function sanitiseExposeFromCompose(portStr: string): string {
portStr: string,
): string {
if (/^[0-9]*$/.test(portStr)) { if (/^[0-9]*$/.test(portStr)) {
return `${portStr}/tcp`; return `${portStr}/tcp`;
} }
return portStr; return portStr;
} }
export function formatDevice( export function formatDevice(deviceStr: string): DockerDevice {
deviceStr: string, const [pathOnHost, ...parts] = deviceStr.split(':');
): DockerDevice { let [pathInContainer, cgroup] = parts;
const [ pathOnHost, ...parts ] = deviceStr.split(':');
let [ pathInContainer, cgroup ] = parts;
if (pathInContainer == null) { if (pathInContainer == null) {
pathInContainer = pathOnHost; pathInContainer = pathOnHost;
} }
@ -300,7 +316,7 @@ export function addFeaturesFromLabels(
service: Service, service: Service,
options: DeviceMetadata, options: DeviceMetadata,
): void { ): void {
const setEnvVariables = function (key: string, val: string) { const setEnvVariables = function(key: string, val: string) {
service.config.environment[`RESIN_${key}`] = val; service.config.environment[`RESIN_${key}`] = val;
service.config.environment[`BALENA_${key}`] = val; service.config.environment[`BALENA_${key}`] = val;
}; };
@ -323,13 +339,19 @@ export function addFeaturesFromLabels(
} }
if (checkTruthy(service.config.labels['io.balena.features.balena-socket'])) { if (checkTruthy(service.config.labels['io.balena.features.balena-socket'])) {
service.config.volumes.push(`${constants.dockerSocket}:${constants.dockerSocket}`); service.config.volumes.push(
`${constants.dockerSocket}:${constants.dockerSocket}`,
);
if (service.config.environment['DOCKER_HOST'] == null) { if (service.config.environment['DOCKER_HOST'] == null) {
service.config.environment['DOCKER_HOST'] = `unix://${constants.dockerSocket}`; service.config.environment['DOCKER_HOST'] = `unix://${
constants.dockerSocket
}`;
} }
// We keep balena.sock for backwards compatibility // We keep balena.sock for backwards compatibility
if (constants.dockerSocket != '/var/run/balena.sock') { if (constants.dockerSocket != '/var/run/balena.sock') {
service.config.volumes.push(`${constants.dockerSocket}:/var/run/balena.sock`); service.config.volumes.push(
`${constants.dockerSocket}:/var/run/balena.sock`,
);
} }
} }
@ -342,72 +364,78 @@ export function addFeaturesFromLabels(
setEnvVariables('SUPERVISOR_API_KEY', options.apiSecret); setEnvVariables('SUPERVISOR_API_KEY', options.apiSecret);
if (service.config.networkMode === 'host') { if (service.config.networkMode === 'host') {
setEnvVariables('SUPERVISOR_HOST', '127.0.0.1'); setEnvVariables('SUPERVISOR_HOST', '127.0.0.1');
setEnvVariables('SUPERVISOR_ADDRESS', `http://127.0.0.1:${options.listenPort}`); setEnvVariables(
'SUPERVISOR_ADDRESS',
`http://127.0.0.1:${options.listenPort}`,
);
} else { } else {
setEnvVariables('SUPERVISOR_HOST', options.supervisorApiHost); setEnvVariables('SUPERVISOR_HOST', options.supervisorApiHost);
setEnvVariables('SUPERVISOR_ADDRESS', `http://${options.supervisorApiHost}:${options.listenPort}`); setEnvVariables(
service.config.networks[constants.supervisorNetworkInterface] = { }; 'SUPERVISOR_ADDRESS',
`http://${options.supervisorApiHost}:${options.listenPort}`,
);
service.config.networks[constants.supervisorNetworkInterface] = {};
} }
} else { } else {
// Ensure that the user hasn't added 'supervisor0' to the service's list // Ensure that the user hasn't added 'supervisor0' to the service's list
// of networks // of networks
delete service.config.networks[constants.supervisorNetworkInterface]; delete service.config.networks[constants.supervisorNetworkInterface];
} }
} }
export function serviceUlimitsToDockerUlimits( export function serviceUlimitsToDockerUlimits(
ulimits: ServiceConfig['ulimits'] | null | undefined, ulimits: ServiceConfig['ulimits'] | null | undefined,
): Array<{ Name: string, Soft: number, Hard: number }> { ): Array<{ Name: string; Soft: number; Hard: number }> {
const ret: Array<{ Name: string; Soft: number; Hard: number }> = [];
const ret: Array<{ Name: string, Soft: number, Hard: number }> = [];
_.each(ulimits, ({ soft, hard }, name) => { _.each(ulimits, ({ soft, hard }, name) => {
ret.push({ Name: name, Soft: soft, Hard: hard }); ret.push({ Name: name, Soft: soft, Hard: hard });
}); });
return ret; return ret;
} }
export function serviceRestartToDockerRestartPolicy(restart: string): { Name: string, MaximumRetryCount: number } { export function serviceRestartToDockerRestartPolicy(
restart: string,
): { Name: string; MaximumRetryCount: number } {
return { return {
Name: restart, Name: restart,
MaximumRetryCount: 0, MaximumRetryCount: 0,
}; };
} }
export function serviceNetworksToDockerNetworks(networks: ServiceConfig['networks']) export function serviceNetworksToDockerNetworks(
: Dockerode.ContainerCreateOptions['NetworkingConfig'] { networks: ServiceConfig['networks'],
): Dockerode.ContainerCreateOptions['NetworkingConfig'] {
const dockerNetworks: Dockerode.ContainerCreateOptions['NetworkingConfig'] = {
EndpointsConfig: {},
};
const dockerNetworks: Dockerode.ContainerCreateOptions['NetworkingConfig'] = { _.each(networks, (net, name) => {
EndpointsConfig: { }, // WHY??? This shouldn't be necessary, as we define it above...
}; if (dockerNetworks.EndpointsConfig != null) {
dockerNetworks.EndpointsConfig[name] = {};
const conf = dockerNetworks.EndpointsConfig[name];
conf.IPAMConfig = {};
conf.Aliases = [];
_.each(net, (v, k) => {
switch (k) {
case 'ipv4Address':
conf.IPAMConfig.IPV4Address = v;
break;
case 'ipv6Address':
conf.IPAMConfig.IPV6Address = v;
break;
case 'linkLocalIps':
conf.IPAMConfig.LinkLocalIps = v;
break;
case 'aliases':
conf.Aliases = v;
break;
}
});
}
});
_.each(networks, (net, name) => { return dockerNetworks;
// WHY??? This shouldn't be necessary, as we define it above...
if (dockerNetworks.EndpointsConfig != null) {
dockerNetworks.EndpointsConfig[name] = { };
const conf = dockerNetworks.EndpointsConfig[name];
conf.IPAMConfig = { };
conf.Aliases = [ ];
_.each(net, (v, k) => {
switch(k) {
case 'ipv4Address':
conf.IPAMConfig.IPV4Address = v;
break;
case 'ipv6Address':
conf.IPAMConfig.IPV6Address = v;
break;
case 'linkLocalIps':
conf.IPAMConfig.LinkLocalIps = v;
break;
case 'aliases':
conf.Aliases = v;
break;
}
});
}
});
return dockerNetworks;
} }
export function dockerNetworkToServiceNetwork( export function dockerNetworkToServiceNetwork(
@ -415,10 +443,10 @@ export function dockerNetworkToServiceNetwork(
): ServiceConfig['networks'] { ): ServiceConfig['networks'] {
// Take the input network object, filter out any nullish fields, extract things to // Take the input network object, filter out any nullish fields, extract things to
// the correct level and return // the correct level and return
const networks: ServiceConfig['networks'] = { }; const networks: ServiceConfig['networks'] = {};
_.each(dockerNetworks, (net, name) => { _.each(dockerNetworks, (net, name) => {
networks[name] = { }; networks[name] = {};
if (net.Aliases != null && !_.isEmpty(net.Aliases)) { if (net.Aliases != null && !_.isEmpty(net.Aliases)) {
networks[name].aliases = net.Aliases; networks[name].aliases = net.Aliases;
} }
@ -444,19 +472,29 @@ export function normalizeNullValues(obj: Dictionary<any>): void {
_.each(obj, (v, k) => { _.each(obj, (v, k) => {
if (v == null) { if (v == null) {
obj[k] = undefined; obj[k] = undefined;
} else if(_.isObject(v)) { } else if (_.isObject(v)) {
normalizeNullValues(v); normalizeNullValues(v);
} }
}); });
} }
export function normalizeLabels( export function normalizeLabels(labels: {
labels: { [key: string]: string }, [key: string]: string;
): { [key: string]: string } { }): { [key: string]: string } {
const legacyLabels = _.mapKeys(_.pickBy(labels, (_v, k) => _.startsWith(k, 'io.resin.')), (_v, k) => { const legacyLabels = _.mapKeys(
return k.replace(/resin/g, 'balena'); // e.g. io.resin.features.resin-api -> io.balena.features.balena-api _.pickBy(labels, (_v, k) => _.startsWith(k, 'io.resin.')),
}); (_v, k) => {
const balenaLabels = _.pickBy(labels, (_v, k) => _.startsWith(k, 'io.balena.')); return k.replace(/resin/g, 'balena'); // e.g. io.resin.features.resin-api -> io.balena.features.balena-api
const otherLabels = _.pickBy(labels, (_v, k) => !(_.startsWith(k, 'io.balena.') || _.startsWith(k, 'io.resin.'))); },
return _.assign({}, otherLabels, legacyLabels, balenaLabels) as { [key: string]: string }; );
const balenaLabels = _.pickBy(labels, (_v, k) =>
_.startsWith(k, 'io.balena.'),
);
const otherLabels = _.pickBy(
labels,
(_v, k) => !(_.startsWith(k, 'io.balena.') || _.startsWith(k, 'io.resin.')),
);
return _.assign({}, otherLabels, legacyLabels, balenaLabels) as {
[key: string]: string;
};
} }

View File

@ -6,7 +6,10 @@ import { generateUniqueKey } from 'resin-register-device';
import ConfigJsonConfigBackend from './config/configJson'; import ConfigJsonConfigBackend from './config/configJson';
import { ConfigProviderFunctions, createProviderFunctions } from './config/functions'; import {
ConfigProviderFunctions,
createProviderFunctions,
} from './config/functions';
import * as constants from './lib/constants'; import * as constants from './lib/constants';
import { ConfigMap, ConfigSchema, ConfigValue } from './lib/types'; import { ConfigMap, ConfigSchema, ConfigValue } from './lib/types';
@ -18,7 +21,6 @@ interface ConfigOpts {
} }
class Config extends EventEmitter { class Config extends EventEmitter {
private db: DB; private db: DB;
private configJsonBackend: ConfigJsonConfigBackend; private configJsonBackend: ConfigJsonConfigBackend;
private providerFunctions: ConfigProviderFunctions; private providerFunctions: ConfigProviderFunctions;
@ -37,8 +39,15 @@ class Config extends EventEmitter {
deviceId: { source: 'config.json', mutable: true }, deviceId: { source: 'config.json', mutable: true },
registered_at: { source: 'config.json', mutable: true }, registered_at: { source: 'config.json', mutable: true },
applicationId: { source: 'config.json' }, applicationId: { source: 'config.json' },
appUpdatePollInterval: { source: 'config.json', mutable: true, default: 60000 }, appUpdatePollInterval: {
mixpanelToken: { source: 'config.json', default: constants.defaultMixpanelToken }, source: 'config.json',
mutable: true,
default: 60000,
},
mixpanelToken: {
source: 'config.json',
default: constants.defaultMixpanelToken,
},
bootstrapRetryDelay: { source: 'config.json', default: 30000 }, bootstrapRetryDelay: { source: 'config.json', default: 30000 },
supervisorOfflineMode: { source: 'config.json', default: false }, supervisorOfflineMode: { source: 'config.json', default: false },
hostname: { source: 'config.json', mutable: true }, hostname: { source: 'config.json', mutable: true },
@ -81,15 +90,17 @@ class Config extends EventEmitter {
public constructor({ db, configPath }: ConfigOpts) { public constructor({ db, configPath }: ConfigOpts) {
super(); super();
this.db = db; this.db = db;
this.configJsonBackend = new ConfigJsonConfigBackend(this.schema, configPath); this.configJsonBackend = new ConfigJsonConfigBackend(
this.schema,
configPath,
);
this.providerFunctions = createProviderFunctions(this); this.providerFunctions = createProviderFunctions(this);
} }
public init(): Bluebird<void> { public init(): Bluebird<void> {
return this.configJsonBackend.init() return this.configJsonBackend.init().then(() => {
.then(() => { return this.generateRequiredFields();
return this.generateRequiredFields(); });
});
} }
public get(key: string, trx?: Transaction): Bluebird<ConfigValue> { public get(key: string, trx?: Transaction): Bluebird<ConfigValue> {
@ -99,71 +110,90 @@ class Config extends EventEmitter {
if (this.schema[key] == null) { if (this.schema[key] == null) {
throw new Error(`Unknown config value ${key}`); throw new Error(`Unknown config value ${key}`);
} }
switch(this.schema[key].source) { switch (this.schema[key].source) {
case 'func': case 'func':
return this.providerFunctions[key].get() return this.providerFunctions[key].get().catch(e => {
.catch((e) => { console.error(`Error getting config value for ${key}`, e, e.stack);
console.error(`Error getting config value for ${key}`, e, e.stack); return null;
return null; });
});
case 'config.json': case 'config.json':
return this.configJsonBackend.get(key); return this.configJsonBackend.get(key);
case 'db': case 'db':
return db('config').select('value').where({ key }) return db('config')
.then(([ conf ]: [{ value: string }]) => { .select('value')
.where({ key })
.then(([conf]: [{ value: string }]) => {
if (conf != null) { if (conf != null) {
return conf.value; return conf.value;
} }
return; return;
}); });
} }
}) }).then(value => {
.then((value) => { const schemaEntry = this.schema[key];
const schemaEntry = this.schema[key]; if (value == null && schemaEntry != null && schemaEntry.default != null) {
if (value == null && schemaEntry != null && schemaEntry.default != null) { return schemaEntry.default;
return schemaEntry.default; }
} return value;
return value; });
});
} }
public getMany(keys: string[], trx?: Transaction): Bluebird<ConfigMap> { public getMany(keys: string[], trx?: Transaction): Bluebird<ConfigMap> {
return Bluebird.map(keys, (key: string) => this.get(key, trx)) return Bluebird.map(keys, (key: string) => this.get(key, trx)).then(
.then((values) => { values => {
return _.zipObject(keys, values); return _.zipObject(keys, values);
}); },
);
} }
public set(keyValues: ConfigMap, trx?: Transaction): Bluebird<void> { public set(keyValues: ConfigMap, trx?: Transaction): Bluebird<void> {
return Bluebird.try(() => { return Bluebird.try(() => {
// Split the values based on which storage backend they use // Split the values based on which storage backend they use
type SplitConfigBackend = { configJsonVals: ConfigMap, dbVals: ConfigMap, fnVals: ConfigMap }; type SplitConfigBackend = {
const { configJsonVals, dbVals, fnVals }: SplitConfigBackend = _.reduce(keyValues, (acc: SplitConfigBackend, val, key) => { configJsonVals: ConfigMap;
if (this.schema[key] == null || !this.schema[key].mutable) { dbVals: ConfigMap;
throw new Error(`Config field ${key} not found or is immutable in config.set`); fnVals: ConfigMap;
} };
if (this.schema[key].source === 'config.json') { const { configJsonVals, dbVals, fnVals }: SplitConfigBackend = _.reduce(
acc.configJsonVals[key] = val; keyValues,
} else if (this.schema[key].source === 'db') { (acc: SplitConfigBackend, val, key) => {
acc.dbVals[key] = val; if (this.schema[key] == null || !this.schema[key].mutable) {
} else if (this.schema[key].source === 'func') { throw new Error(
acc.fnVals[key] = val; `Config field ${key} not found or is immutable in config.set`,
} else { );
throw new Error(`Unknown config backend for key: ${key}, backend: ${this.schema[key].source}`); }
} if (this.schema[key].source === 'config.json') {
return acc; acc.configJsonVals[key] = val;
}, { configJsonVals: { }, dbVals: { }, fnVals: { } }); } else if (this.schema[key].source === 'db') {
acc.dbVals[key] = val;
} else if (this.schema[key].source === 'func') {
acc.fnVals[key] = val;
} else {
throw new Error(
`Unknown config backend for key: ${key}, backend: ${
this.schema[key].source
}`,
);
}
return acc;
},
{ configJsonVals: {}, dbVals: {}, fnVals: {} },
);
// Set these values, taking into account the knex transaction // Set these values, taking into account the knex transaction
const setValuesInTransaction = (tx: Transaction): Bluebird<void> => { const setValuesInTransaction = (tx: Transaction): Bluebird<void> => {
const dbKeys = _.keys(dbVals); const dbKeys = _.keys(dbVals);
return this.getMany(dbKeys, tx) return this.getMany(dbKeys, tx)
.then((oldValues) => { .then(oldValues => {
return Bluebird.map(dbKeys, (key: string) => { return Bluebird.map(dbKeys, (key: string) => {
const value = dbVals[key]; const value = dbVals[key];
if (oldValues[key] !== value) { if (oldValues[key] !== value) {
return this.db.upsertModel('config', { key, value }, { key }, tx); return this.db.upsertModel(
'config',
{ key, value },
{ key },
tx,
);
} }
}); });
}) })
@ -171,7 +201,9 @@ class Config extends EventEmitter {
return Bluebird.map(_.toPairs(fnVals), ([key, value]) => { return Bluebird.map(_.toPairs(fnVals), ([key, value]) => {
const fn = this.providerFunctions[key]; const fn = this.providerFunctions[key];
if (fn.set == null) { if (fn.set == null) {
throw new Error(`Attempting to set provider function without set() method implemented - key: ${key}`); throw new Error(
`Attempting to set provider function without set() method implemented - key: ${key}`,
);
} }
return fn.set(value, tx); return fn.set(value, tx);
}); });
@ -186,11 +218,12 @@ class Config extends EventEmitter {
if (trx != null) { if (trx != null) {
return setValuesInTransaction(trx).return(); return setValuesInTransaction(trx).return();
} else { } else {
return this.db.transaction((tx) => { return this.db
return setValuesInTransaction(tx); .transaction(tx => {
}).return(); return setValuesInTransaction(tx);
})
.return();
} }
}) })
.then(() => { .then(() => {
return setImmediate(() => { return setImmediate(() => {
@ -203,23 +236,34 @@ class Config extends EventEmitter {
public remove(key: string): Bluebird<void> { public remove(key: string): Bluebird<void> {
return Bluebird.try(() => { return Bluebird.try(() => {
if (this.schema[key] == null || !this.schema[key].mutable) { if (this.schema[key] == null || !this.schema[key].mutable) {
throw new Error(`Attempt to delete non-existent or immutable key ${key}`); throw new Error(
`Attempt to delete non-existent or immutable key ${key}`,
);
} }
if (this.schema[key].source === 'config.json') { if (this.schema[key].source === 'config.json') {
return this.configJsonBackend.remove(key); return this.configJsonBackend.remove(key);
} else if (this.schema[key].source === 'db') { } else if (this.schema[key].source === 'db') {
return this.db.models('config').del().where({ key }); return this.db
.models('config')
.del()
.where({ key });
} else if (this.schema[key].source === 'func') { } else if (this.schema[key].source === 'func') {
const mutFn = this.providerFunctions[key]; const mutFn = this.providerFunctions[key];
if (mutFn == null) { if (mutFn == null) {
throw new Error(`Could not find provider function for config ${key}!`); throw new Error(
`Could not find provider function for config ${key}!`,
);
} }
if (mutFn.remove == null) { if (mutFn.remove == null) {
throw new Error(`Could not find removal provider function for config ${key}`); throw new Error(
`Could not find removal provider function for config ${key}`,
);
} }
return mutFn.remove(); return mutFn.remove();
} else { } else {
throw new Error(`Unknown or unsupported config backend: ${this.schema[key].source}`); throw new Error(
`Unknown or unsupported config backend: ${this.schema[key].source}`,
);
} }
}); });
} }
@ -241,23 +285,21 @@ class Config extends EventEmitter {
'deviceApiKey', 'deviceApiKey',
'apiSecret', 'apiSecret',
'offlineMode', 'offlineMode',
]) ]).then(({ uuid, deviceApiKey, apiSecret, offlineMode }) => {
.then(({ uuid, deviceApiKey, apiSecret, offlineMode }) => { // These fields need to be set regardless
// These fields need to be set regardless if (uuid == null || apiSecret == null) {
if (uuid == null || apiSecret == null) { uuid = uuid || this.newUniqueKey();
uuid = uuid || this.newUniqueKey(); apiSecret = apiSecret || this.newUniqueKey();
apiSecret = apiSecret || this.newUniqueKey(); }
return this.set({ uuid, apiSecret }).then(() => {
if (offlineMode) {
return;
}
if (deviceApiKey == null) {
return this.set({ deviceApiKey: this.newUniqueKey() });
} }
return this.set({ uuid, apiSecret })
.then(() => {
if (offlineMode) {
return;
}
if (deviceApiKey == null) {
return this.set({ deviceApiKey: this.newUniqueKey() });
}
});
}); });
});
} }
} }

View File

@ -26,7 +26,13 @@ const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountPoint}`;
function remountAndWriteAtomic(file: string, data: string): Promise<void> { function remountAndWriteAtomic(file: string, data: string): Promise<void> {
// TODO: Find out why the below Promise.resolve() is required // TODO: Find out why the below Promise.resolve() is required
// Here's the dangerous part: // Here's the dangerous part:
return Promise.resolve(childProcess.execAsync(`mount -t vfat -o remount,rw ${constants.bootBlockDevice} ${bootMountPoint}`)) return Promise.resolve(
childProcess.execAsync(
`mount -t vfat -o remount,rw ${
constants.bootBlockDevice
} ${bootMountPoint}`,
),
)
.then(() => { .then(() => {
return fsUtils.writeFileAtomic(file, data); return fsUtils.writeFileAtomic(file, data);
}) })
@ -34,7 +40,6 @@ function remountAndWriteAtomic(file: string, data: string): Promise<void> {
} }
export abstract class DeviceConfigBackend { export abstract class DeviceConfigBackend {
// Does this config backend support the given device type? // Does this config backend support the given device type?
public abstract matches(deviceType: string): boolean; public abstract matches(deviceType: string): boolean;
@ -59,17 +64,24 @@ export abstract class DeviceConfigBackend {
// Process the value if the environment variable, ready to be written to // Process the value if the environment variable, ready to be written to
// the backend // the backend
public abstract processConfigVarValue(key: string, value: string): string | string[]; public abstract processConfigVarValue(
key: string,
value: string,
): string | string[];
// Return the env var name for this config option // Return the env var name for this config option
public abstract createConfigVarName(configName: string): string; public abstract createConfigVarName(configName: string): string;
} }
export class RPiConfigBackend extends DeviceConfigBackend { export class RPiConfigBackend extends DeviceConfigBackend {
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}CONFIG_`; private static bootConfigVarPrefix = `${
constants.hostConfigVarPrefix
}CONFIG_`;
private static bootConfigPath = `${bootMountPoint}/config.txt`; private static bootConfigPath = `${bootMountPoint}/config.txt`;
public static bootConfigVarRegex = new RegExp('(' + _.escapeRegExp(RPiConfigBackend.bootConfigVarPrefix) + ')(.+)'); public static bootConfigVarRegex = new RegExp(
'(' + _.escapeRegExp(RPiConfigBackend.bootConfigVarPrefix) + ')(.+)',
);
private static arrayConfigKeys = [ private static arrayConfigKeys = [
'dtparam', 'dtparam',
@ -97,45 +109,46 @@ export class RPiConfigBackend extends DeviceConfigBackend {
} }
public getBootConfig(): Promise<ConfigOptions> { public getBootConfig(): Promise<ConfigOptions> {
return Promise.resolve(fs.readFile(RPiConfigBackend.bootConfigPath, 'utf-8')) return Promise.resolve(
.then((confStr) => { fs.readFile(RPiConfigBackend.bootConfigPath, 'utf-8'),
).then(confStr => {
const conf: ConfigOptions = {};
const configStatements = confStr.split(/\r?\n/);
const conf: ConfigOptions = { }; for (const configStr of configStatements) {
const configStatements = confStr.split(/\r?\n/); // Don't show warnings for comments and empty lines
const trimmed = _.trimStart(configStr);
for (const configStr of configStatements) { if (_.startsWith(trimmed, '#') || trimmed === '') {
// Don't show warnings for comments and empty lines continue;
const trimmed = _.trimStart(configStr); }
if (_.startsWith(trimmed, '#') || trimmed === '') { let keyValue = /^([^=]+)=(.*)$/.exec(configStr);
continue; if (keyValue != null) {
} const [, key, value] = keyValue;
let keyValue = /^([^=]+)=(.*)$/.exec(configStr); if (!_.includes(RPiConfigBackend.arrayConfigKeys, key)) {
if (keyValue != null) {
const [ , key, value ] = keyValue;
if (!_.includes(RPiConfigBackend.arrayConfigKeys, key)) {
conf[key] = value;
} else {
if (conf[key] == null) {
conf[key] = [];
}
(conf[key] as string[]).push(value);
}
continue;
}
// Try the next regex instead
keyValue = /^(initramfs) (.+)/.exec(configStr);
if (keyValue != null) {
const [ , key, value ] = keyValue;
conf[key] = value; conf[key] = value;
} else { } else {
console.log(`Warning - Could not parse config.txt entry: ${configStr}. Ignoring.`); if (conf[key] == null) {
conf[key] = [];
}
(conf[key] as string[]).push(value);
} }
continue;
} }
return conf; // Try the next regex instead
}); keyValue = /^(initramfs) (.+)/.exec(configStr);
if (keyValue != null) {
const [, key, value] = keyValue;
conf[key] = value;
} else {
console.log(
`Warning - Could not parse config.txt entry: ${configStr}. Ignoring.`,
);
}
}
return conf;
});
} }
public setBootConfig(opts: ConfigOptions): Promise<void> { public setBootConfig(opts: ConfigOptions): Promise<void> {
@ -144,8 +157,10 @@ export class RPiConfigBackend extends DeviceConfigBackend {
_.each(opts, (value, key) => { _.each(opts, (value, key) => {
if (key === 'initramfs') { if (key === 'initramfs') {
confStatements.push(`${key} ${value}`); confStatements.push(`${key} ${value}`);
} else if(_.isArray(value)) { } else if (_.isArray(value)) {
confStatements = confStatements.concat(_.map(value, (entry) => `${key}=${entry}`)); confStatements = confStatements.concat(
_.map(value, entry => `${key}=${entry}`),
);
} else { } else {
confStatements.push(`${key}=${value}`); confStatements.push(`${key}=${value}`);
} }
@ -171,7 +186,7 @@ export class RPiConfigBackend extends DeviceConfigBackend {
public processConfigVarValue(key: string, value: string): string | string[] { public processConfigVarValue(key: string, value: string): string | string[] {
if (_.includes(RPiConfigBackend.arrayConfigKeys, key)) { if (_.includes(RPiConfigBackend.arrayConfigKeys, key)) {
if (!_.startsWith(value, '"')) { if (!_.startsWith(value, '"')) {
return [ value ]; return [value];
} else { } else {
return JSON.parse(`[${value}]`); return JSON.parse(`[${value}]`);
} }
@ -185,99 +200,122 @@ export class RPiConfigBackend extends DeviceConfigBackend {
} }
export class ExtlinuxConfigBackend extends DeviceConfigBackend { export class ExtlinuxConfigBackend extends DeviceConfigBackend {
private static bootConfigVarPrefix = `${constants.hostConfigVarPrefix}EXTLINUX_`; private static bootConfigVarPrefix = `${
constants.hostConfigVarPrefix
}EXTLINUX_`;
private static bootConfigPath = `${bootMountPoint}/extlinux/extlinux.conf`; private static bootConfigPath = `${bootMountPoint}/extlinux/extlinux.conf`;
public static bootConfigVarRegex = new RegExp('(' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)'); public static bootConfigVarRegex = new RegExp(
'(' + _.escapeRegExp(ExtlinuxConfigBackend.bootConfigVarPrefix) + ')(.+)',
);
private static suppportedConfigKeys = [ private static suppportedConfigKeys = ['isolcpus'];
'isolcpus',
];
public matches(deviceType: string): boolean { public matches(deviceType: string): boolean {
return _.startsWith(deviceType, 'jetson-tx'); return _.startsWith(deviceType, 'jetson-tx');
} }
public getBootConfig(): Promise<ConfigOptions> { public getBootConfig(): Promise<ConfigOptions> {
return Promise.resolve(fs.readFile(ExtlinuxConfigBackend.bootConfigPath, 'utf-8')) return Promise.resolve(
.then((confStr) => { fs.readFile(ExtlinuxConfigBackend.bootConfigPath, 'utf-8'),
const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(confStr); ).then(confStr => {
const parsedBootFile = ExtlinuxConfigBackend.parseExtlinuxFile(confStr);
// First find the default label name // First find the default label name
const defaultLabel = _.find(parsedBootFile.globals, (_v, l) => { const defaultLabel = _.find(parsedBootFile.globals, (_v, l) => {
if (l === 'DEFAULT') { if (l === 'DEFAULT') {
return true; return true;
}
return false;
});
if (defaultLabel == null) {
throw new Error('Could not find default entry for extlinux.conf file');
} }
return false;
const labelEntry = parsedBootFile.labels[defaultLabel];
if (labelEntry == null) {
throw new Error(`Cannot find default label entry (label: ${defaultLabel}) for extlinux.conf file`);
}
// All configuration options come from the `APPEND` directive in the default label entry
const appendEntry = labelEntry.APPEND;
if (appendEntry == null) {
throw new Error('Could not find APPEND directive in default extlinux.conf boot entry');
}
const conf: ConfigOptions = { };
const values = appendEntry.split(' ');
for(const value of values) {
const parts = value.split('=');
if (this.isSupportedConfig(parts[0])) {
if (parts.length !== 2) {
throw new Error(`Could not parse extlinux configuration entry: ${values} [value with error: ${value}]`);
}
conf[parts[0]] = parts[1];
}
}
return conf;
}); });
if (defaultLabel == null) {
throw new Error('Could not find default entry for extlinux.conf file');
}
const labelEntry = parsedBootFile.labels[defaultLabel];
if (labelEntry == null) {
throw new Error(
`Cannot find default label entry (label: ${defaultLabel}) for extlinux.conf file`,
);
}
// All configuration options come from the `APPEND` directive in the default label entry
const appendEntry = labelEntry.APPEND;
if (appendEntry == null) {
throw new Error(
'Could not find APPEND directive in default extlinux.conf boot entry',
);
}
const conf: ConfigOptions = {};
const values = appendEntry.split(' ');
for (const value of values) {
const parts = value.split('=');
if (this.isSupportedConfig(parts[0])) {
if (parts.length !== 2) {
throw new Error(
`Could not parse extlinux configuration entry: ${values} [value with error: ${value}]`,
);
}
conf[parts[0]] = parts[1];
}
}
return conf;
});
} }
public setBootConfig(opts: ConfigOptions): Promise<void> { public setBootConfig(opts: ConfigOptions): Promise<void> {
// First get a representation of the configuration file, with all balena-supported configuration removed // First get a representation of the configuration file, with all balena-supported configuration removed
return Promise.resolve(fs.readFile(ExtlinuxConfigBackend.bootConfigPath)) return Promise.resolve(
.then((data) => { fs.readFile(ExtlinuxConfigBackend.bootConfigPath),
const extlinuxFile = ExtlinuxConfigBackend.parseExtlinuxFile(data.toString()); ).then(data => {
const defaultLabel = extlinuxFile.globals.DEFAULT; const extlinuxFile = ExtlinuxConfigBackend.parseExtlinuxFile(
if (defaultLabel == null) { data.toString(),
throw new Error('Could not find DEFAULT directive entry in extlinux.conf'); );
} const defaultLabel = extlinuxFile.globals.DEFAULT;
const defaultEntry = extlinuxFile.labels[defaultLabel]; if (defaultLabel == null) {
if (defaultEntry == null) { throw new Error(
throw new Error(`Could not find default extlinux.conf entry: ${defaultLabel}`); 'Could not find DEFAULT directive entry in extlinux.conf',
} );
}
const defaultEntry = extlinuxFile.labels[defaultLabel];
if (defaultEntry == null) {
throw new Error(
`Could not find default extlinux.conf entry: ${defaultLabel}`,
);
}
if (defaultEntry.APPEND == null) { if (defaultEntry.APPEND == null) {
throw new Error(`extlinux.conf APPEND directive not found for default entry: ${defaultLabel}, not sure how to proceed!`); throw new Error(
} `extlinux.conf APPEND directive not found for default entry: ${defaultLabel}, not sure how to proceed!`,
);
}
const appendLine = _.filter(defaultEntry.APPEND.split(' '), (entry) => { const appendLine = _.filter(defaultEntry.APPEND.split(' '), entry => {
const lhs = entry.split('='); const lhs = entry.split('=');
return !this.isSupportedConfig(lhs[0]); return !this.isSupportedConfig(lhs[0]);
});
// Apply the new configuration to the "plain" append line above
_.each(opts, (value, key) => {
appendLine.push(`${key}=${value}`);
});
defaultEntry.APPEND = appendLine.join(' ');
const extlinuxString = ExtlinuxConfigBackend.extlinuxFileToString(extlinuxFile);
return remountAndWriteAtomic(ExtlinuxConfigBackend.bootConfigPath, extlinuxString);
}); });
// Apply the new configuration to the "plain" append line above
_.each(opts, (value, key) => {
appendLine.push(`${key}=${value}`);
});
defaultEntry.APPEND = appendLine.join(' ');
const extlinuxString = ExtlinuxConfigBackend.extlinuxFileToString(
extlinuxFile,
);
return remountAndWriteAtomic(
ExtlinuxConfigBackend.bootConfigPath,
extlinuxString,
);
});
} }
public isSupportedConfig(configName: string): boolean { public isSupportedConfig(configName: string): boolean {
@ -301,15 +339,14 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
} }
private static parseExtlinuxFile(confStr: string): ExtlinuxFile { private static parseExtlinuxFile(confStr: string): ExtlinuxFile {
const file: ExtlinuxFile = { const file: ExtlinuxFile = {
globals: { }, globals: {},
labels: { }, labels: {},
}; };
// Firstly split by line and filter any comments and empty lines // Firstly split by line and filter any comments and empty lines
let lines = confStr.split(/\r?\n/); let lines = confStr.split(/\r?\n/);
lines = _.filter(lines, (l) => { lines = _.filter(lines, l => {
const trimmed = _.trimStart(l); const trimmed = _.trimStart(l);
return trimmed !== '' && !_.startsWith(trimmed, '#'); return trimmed !== '' && !_.startsWith(trimmed, '#');
}); });
@ -342,9 +379,8 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
} }
} else { } else {
lastLabel = value; lastLabel = value;
file.labels[lastLabel] = { }; file.labels[lastLabel] = {};
} }
} }
return file; return file;
@ -363,5 +399,4 @@ export class ExtlinuxConfigBackend extends DeviceConfigBackend {
}); });
return ret; return ret;
} }
} }

View File

@ -13,58 +13,60 @@ import * as osRelease from '../lib/os-release';
type LockCallback = (file: string) => Promise<() => void>; type LockCallback = (file: string) => Promise<() => void>;
export default class ConfigJsonConfigBackend { export default class ConfigJsonConfigBackend {
private lock: Lock; private lock: Lock;
private readLockConfigJson: () => Promise.Disposer<() => void>; private readLockConfigJson: () => Promise.Disposer<() => void>;
private writeLockConfigJson: () => Promise.Disposer<() => void>; private writeLockConfigJson: () => Promise.Disposer<() => void>;
private configPath?: string; private configPath?: string;
private cache: { [key: string]: ConfigValue } = { }; private cache: { [key: string]: ConfigValue } = {};
private schema: ConfigSchema; private schema: ConfigSchema;
public constructor(schema: ConfigSchema, configPath?: string) { public constructor(schema: ConfigSchema, configPath?: string) {
this.configPath = configPath; this.configPath = configPath;
this.schema = schema; this.schema = schema;
this.lock = new Lock(); this.lock = new Lock();
const writeLock: LockCallback = Promise.promisify(this.lock.async.writeLock); const writeLock: LockCallback = Promise.promisify(
const readLock: LockCallback = Promise.promisify(this.lock.async.readLock); this.lock.async.writeLock,
this.writeLockConfigJson = () => writeLock('config.json').disposer((release) => release()); );
this.readLockConfigJson = () => readLock('config.json').disposer((release) => release()); const readLock: LockCallback = Promise.promisify(this.lock.async.readLock);
this.writeLockConfigJson = () =>
writeLock('config.json').disposer(release => release());
this.readLockConfigJson = () =>
readLock('config.json').disposer(release => release());
} }
public init(): Promise<void> { public init(): Promise<void> {
return this.read() return this.read().then(configJson => {
.then((configJson) => { _.assign(this.cache, configJson);
_.assign(this.cache, configJson); });
});
} }
public set(keyVals: { [key: string]: ConfigValue }): Promise<void> { public set(keyVals: { [key: string]: ConfigValue }): Promise<void> {
let changed = false; let changed = false;
return Promise.using(this.writeLockConfigJson(), () => { return Promise.using(this.writeLockConfigJson(), () => {
return Promise.mapSeries(_.keys(keyVals), (key: string) => { return Promise.mapSeries(_.keys(keyVals), (key: string) => {
const value = keyVals[key]; const value = keyVals[key];
if (this.cache[key] !== value) { if (this.cache[key] !== value) {
this.cache[key] = value; this.cache[key] = value;
if (value == null && this.schema[key] != null && this.schema[key].removeIfNull) { if (
value == null &&
this.schema[key] != null &&
this.schema[key].removeIfNull
) {
delete this.cache[key]; delete this.cache[key];
} }
changed = true; changed = true;
} }
}) }).then(() => {
.then(() => { if (changed) {
if (changed) { return this.write();
return this.write(); }
} });
});
}); });
} }
@ -91,22 +93,21 @@ export default class ConfigJsonConfigBackend {
} }
public path(): Promise<string> { public path(): Promise<string> {
return this.pathOnHost() return this.pathOnHost().catch(err => {
.catch((err) => { console.error(err.message);
console.error(err.message); return constants.configJsonNonAtomicPath;
return constants.configJsonNonAtomicPath; });
});
} }
private write(): Promise<void> { private write(): Promise<void> {
let atomicWritePossible = true; let atomicWritePossible = true;
return this.pathOnHost() return this.pathOnHost()
.catch((err) => { .catch(err => {
console.error(err.message); console.error(err.message);
atomicWritePossible = false; atomicWritePossible = false;
return constants.configJsonNonAtomicPath; return constants.configJsonNonAtomicPath;
}) })
.then((configPath) => { .then(configPath => {
if (atomicWritePossible) { if (atomicWritePossible) {
return writeFileAtomic(configPath, JSON.stringify(this.cache)); return writeFileAtomic(configPath, JSON.stringify(this.cache));
} else { } else {
@ -117,7 +118,7 @@ export default class ConfigJsonConfigBackend {
private read(): Promise<string> { private read(): Promise<string> {
return this.path() return this.path()
.then((filename) => { .then(filename => {
return fs.readFile(filename, 'utf-8'); return fs.readFile(filename, 'utf-8');
}) })
.then(JSON.parse); .then(JSON.parse);
@ -130,9 +131,9 @@ export default class ConfigJsonConfigBackend {
if (constants.configJsonPathOnHost != null) { if (constants.configJsonPathOnHost != null) {
return constants.configJsonPathOnHost; return constants.configJsonPathOnHost;
} }
return osRelease.getOSVersion(constants.hostOSVersionPath) return osRelease
.then((osVersion) => { .getOSVersion(constants.hostOSVersionPath)
.then(osVersion => {
if (osVersion == null) { if (osVersion == null) {
throw new Error('Failed to detect OS version!'); throw new Error('Failed to detect OS version!');
} }
@ -147,13 +148,13 @@ export default class ConfigJsonConfigBackend {
// In non-resinOS hosts (or older than 1.0.0), if CONFIG_JSON_PATH wasn't passed // In non-resinOS hosts (or older than 1.0.0), if CONFIG_JSON_PATH wasn't passed
// then we can't do atomic changes (only access to config.json we have is in /boot, // then we can't do atomic changes (only access to config.json we have is in /boot,
// which is assumed to be a file bind mount where rename is impossible) // which is assumed to be a file bind mount where rename is impossible)
throw new Error('Could not determine config.json path on host, atomic write will not be possible'); throw new Error(
'Could not determine config.json path on host, atomic write will not be possible',
);
} }
}); });
}) }).then(file => {
.then((file) => { return path.join(constants.rootMountPoint, file);
return path.join(constants.rootMountPoint, file); });
});
} }
} }

View File

@ -11,7 +11,10 @@ import { ConfigValue } from '../lib/types';
// A provider for schema entries with source 'func' // A provider for schema entries with source 'func'
type ConfigProviderFunctionGetter = () => Bluebird<any>; type ConfigProviderFunctionGetter = () => Bluebird<any>;
type ConfigProviderFunctionSetter = (value: ConfigValue, tx?: Transaction) => Bluebird<void>; type ConfigProviderFunctionSetter = (
value: ConfigValue,
tx?: Transaction,
) => Bluebird<void>;
type ConfigProviderFunctionRemover = () => Bluebird<void>; type ConfigProviderFunctionRemover = () => Bluebird<void>;
interface ConfigProviderFunction { interface ConfigProviderFunction {
@ -24,7 +27,9 @@ export interface ConfigProviderFunctions {
[key: string]: ConfigProviderFunction; [key: string]: ConfigProviderFunction;
} }
export function createProviderFunctions(config: Config): ConfigProviderFunctions { export function createProviderFunctions(
config: Config,
): ConfigProviderFunctions {
return { return {
version: { version: {
get: () => { get: () => {
@ -33,7 +38,8 @@ export function createProviderFunctions(config: Config): ConfigProviderFunctions
}, },
currentApiKey: { currentApiKey: {
get: () => { get: () => {
return config.getMany([ 'apiKey', 'deviceApiKey' ]) return config
.getMany(['apiKey', 'deviceApiKey'])
.then(({ apiKey, deviceApiKey }) => { .then(({ apiKey, deviceApiKey }) => {
return apiKey || deviceApiKey; return apiKey || deviceApiKey;
}); });
@ -41,7 +47,8 @@ export function createProviderFunctions(config: Config): ConfigProviderFunctions
}, },
offlineMode: { offlineMode: {
get: () => { get: () => {
return config.getMany([ 'apiEndpoint', 'supervisorOfflineMode' ]) return config
.getMany(['apiEndpoint', 'supervisorOfflineMode'])
.then(({ apiEndpoint, supervisorOfflineMode }) => { .then(({ apiEndpoint, supervisorOfflineMode }) => {
return Boolean(supervisorOfflineMode) || !Boolean(apiEndpoint); return Boolean(supervisorOfflineMode) || !Boolean(apiEndpoint);
}); });
@ -49,13 +56,9 @@ export function createProviderFunctions(config: Config): ConfigProviderFunctions
}, },
provisioned: { provisioned: {
get: () => { get: () => {
return config.getMany([ return config
'uuid', .getMany(['uuid', 'apiEndpoint', 'registered_at', 'deviceId'])
'apiEndpoint', .then(requiredValues => {
'registered_at',
'deviceId',
])
.then((requiredValues) => {
return _.every(_.values(requiredValues), Boolean); return _.every(_.values(requiredValues), Boolean);
}); });
}, },
@ -72,39 +75,40 @@ export function createProviderFunctions(config: Config): ConfigProviderFunctions
}, },
provisioningOptions: { provisioningOptions: {
get: () => { get: () => {
return config.getMany([ return config
'uuid', .getMany([
'userId', 'uuid',
'applicationId', 'userId',
'apiKey', 'applicationId',
'deviceApiKey', 'apiKey',
'deviceType', 'deviceApiKey',
'apiEndpoint', 'deviceType',
'apiTimeout', 'apiEndpoint',
'registered_at', 'apiTimeout',
'deviceId', 'registered_at',
]).then((conf) => { 'deviceId',
return { ])
uuid: conf.uuid, .then(conf => {
applicationId: conf.applicationId, return {
userId: conf.userId, uuid: conf.uuid,
deviceType: conf.deviceType, applicationId: conf.applicationId,
provisioningApiKey: conf.apiKey, userId: conf.userId,
deviceApiKey: conf.deviceApiKey, deviceType: conf.deviceType,
apiEndpoint: conf.apiEndpoint, provisioningApiKey: conf.apiKey,
apiTimeout: conf.apiTimeout, deviceApiKey: conf.deviceApiKey,
registered_at: conf.registered_at, apiEndpoint: conf.apiEndpoint,
deviceId: conf.deviceId, apiTimeout: conf.apiTimeout,
}; registered_at: conf.registered_at,
}); deviceId: conf.deviceId,
};
});
}, },
}, },
mixpanelHost: { mixpanelHost: {
get: () => { get: () => {
return config.get('apiEndpoint') return config.get('apiEndpoint').then(apiEndpoint => {
.then((apiEndpoint) => { return `${apiEndpoint}/mixpanel`;
return `${apiEndpoint}/mixpanel`; });
});
}, },
}, },
extendedEnvOptions: { extendedEnvOptions: {

View File

@ -8,33 +8,32 @@ import {
RPiConfigBackend, RPiConfigBackend,
} from './backend'; } from './backend';
const configBackends = [new ExtlinuxConfigBackend(), new RPiConfigBackend()];
const configBackends = [
new ExtlinuxConfigBackend(),
new RPiConfigBackend(),
];
export function isConfigDeviceType(deviceType: string): boolean { export function isConfigDeviceType(deviceType: string): boolean {
return getConfigBackend(deviceType) != null; return getConfigBackend(deviceType) != null;
} }
export function getConfigBackend(deviceType: string): DeviceConfigBackend | undefined { export function getConfigBackend(
return _.find(configBackends, (backend) => backend.matches(deviceType)); deviceType: string,
): DeviceConfigBackend | undefined {
return _.find(configBackends, backend => backend.matches(deviceType));
} }
export function envToBootConfig( export function envToBootConfig(
configBackend: DeviceConfigBackend | null, configBackend: DeviceConfigBackend | null,
env: EnvVarObject, env: EnvVarObject,
): ConfigOptions { ): ConfigOptions {
if (configBackend == null) { if (configBackend == null) {
return { }; return {};
} }
return _(env) return _(env)
.pickBy((_val, key) => configBackend.isBootConfigVar(key)) .pickBy((_val, key) => configBackend.isBootConfigVar(key))
.mapKeys((_val, key) => configBackend.processConfigVarName(key)) .mapKeys((_val, key) => configBackend.processConfigVarName(key))
.mapValues((val, key) => configBackend.processConfigVarValue(key, val || '')) .mapValues((val, key) =>
configBackend.processConfigVarValue(key, val || ''),
)
.value(); .value();
} }
@ -42,10 +41,9 @@ export function bootConfigToEnv(
configBackend: DeviceConfigBackend, configBackend: DeviceConfigBackend,
config: ConfigOptions, config: ConfigOptions,
): EnvVarObject { ): EnvVarObject {
return _(config) return _(config)
.mapKeys((_val, key) => configBackend.createConfigVarName(key)) .mapKeys((_val, key) => configBackend.createConfigVarName(key))
.mapValues((val) => { .mapValues(val => {
if (_.isArray(val)) { if (_.isArray(val)) {
return JSON.stringify(val).replace(/^\[(.*)\]$/, '$1'); return JSON.stringify(val).replace(/^\[(.*)\]$/, '$1');
} }
@ -58,11 +56,14 @@ function filterNamespaceFromConfig(
namespace: RegExp, namespace: RegExp,
conf: { [key: string]: any }, conf: { [key: string]: any },
): { [key: string]: any } { ): { [key: string]: any } {
return _.mapKeys(_.pickBy(conf, (_v, k) => { return _.mapKeys(
return namespace.test(k); _.pickBy(conf, (_v, k) => {
}), (_v,k) => { return namespace.test(k);
return k.replace(namespace, '$1'); }),
}); (_v, k) => {
return k.replace(namespace, '$1');
},
);
} }
export function formatConfigKeys( export function formatConfigKeys(
@ -70,18 +71,27 @@ export function formatConfigKeys(
allowedKeys: string[], allowedKeys: string[],
conf: { [key: string]: any }, conf: { [key: string]: any },
): { [key: string]: any } { ): { [key: string]: any } {
const isConfigType = configBackend != null; const isConfigType = configBackend != null;
const namespaceRegex = /^BALENA_(.*)/; const namespaceRegex = /^BALENA_(.*)/;
const legacyNamespaceRegex = /^RESIN_(.*)/; const legacyNamespaceRegex = /^RESIN_(.*)/;
const confFromNamespace = filterNamespaceFromConfig(namespaceRegex, conf); const confFromNamespace = filterNamespaceFromConfig(namespaceRegex, conf);
const confFromLegacyNamespace = filterNamespaceFromConfig(legacyNamespaceRegex, conf); const confFromLegacyNamespace = filterNamespaceFromConfig(
const noNamespaceConf = _.pickBy(conf, (_v,k) => { legacyNamespaceRegex,
conf,
);
const noNamespaceConf = _.pickBy(conf, (_v, k) => {
return !_.startsWith(k, 'RESIN_') && !_.startsWith(k, 'BALENA_'); return !_.startsWith(k, 'RESIN_') && !_.startsWith(k, 'BALENA_');
}); });
const confWithoutNamespace = _.defaults(confFromNamespace, confFromLegacyNamespace, noNamespaceConf); const confWithoutNamespace = _.defaults(
confFromNamespace,
confFromLegacyNamespace,
noNamespaceConf,
);
return _.pickBy(confWithoutNamespace, (_v, k) => { return _.pickBy(confWithoutNamespace, (_v, k) => {
return _.includes(allowedKeys, k) || (isConfigType && configBackend!.isBootConfigVar(k)); return (
_.includes(allowedKeys, k) ||
(isConfigType && configBackend!.isBootConfigVar(k))
);
}); });
} }

View File

@ -23,12 +23,14 @@ class DB {
}, },
useNullAsDefault: true, useNullAsDefault: true,
}); });
} }
public init(): Bluebird<void> { public init(): Bluebird<void> {
return this.knex('knex_migrations_lock').update({ is_locked: 0}) return this.knex('knex_migrations_lock')
.catch(() => { return; }) .update({ is_locked: 0 })
.catch(() => {
return;
})
.then(() => { .then(() => {
return this.knex.migrate.latest({ return this.knex.migrate.latest({
directory: path.join(__dirname, 'migrations'), directory: path.join(__dirname, 'migrations'),
@ -43,13 +45,14 @@ class DB {
public upsertModel( public upsertModel(
modelName: string, modelName: string,
obj: any, obj: any,
id: number | { [key: string]: string}, id: number | { [key: string]: string },
trx?: Knex.Transaction, trx?: Knex.Transaction,
): Bluebird<any> { ): Bluebird<any> {
const knex = trx || this.knex; const knex = trx || this.knex;
return knex(modelName).update(obj).where(id) return knex(modelName)
.update(obj)
.where(id)
.then((n: number) => { .then((n: number) => {
if (n === 0) { if (n === 0) {
return knex(modelName).insert(obj); return knex(modelName).insert(obj);
@ -60,7 +63,6 @@ class DB {
public transaction(cb: DBTransactionCallback): Bluebird<Knex.Transaction> { public transaction(cb: DBTransactionCallback): Bluebird<Knex.Transaction> {
return this.knex.transaction(cb); return this.knex.transaction(cb);
} }
} }
export = DB; export = DB;

View File

@ -9,9 +9,17 @@ export interface ServiceAction {
options: any; options: any;
} }
declare function doRestart(applications: ApplicationManager, appId: number, force: boolean): Promise<void>; declare function doRestart(
applications: ApplicationManager,
appId: number,
force: boolean,
): Promise<void>;
declare function doPurge(applications: ApplicationManager, appId: number, force: boolean): Promise<void>; declare function doPurge(
applications: ApplicationManager,
appId: number,
force: boolean,
): Promise<void>;
declare function serviceAction( declare function serviceAction(
action: string, action: string,

View File

@ -12,7 +12,6 @@ import { doPurge, doRestart, serviceAction } from './common';
import supervisorVersion = require('../lib/supervisor-version'); import supervisorVersion = require('../lib/supervisor-version');
export function createV2Api(router: Router, applications: ApplicationManager) { export function createV2Api(router: Router, applications: ApplicationManager) {
const { _lockingIfNecessary, deviceState } = applications; const { _lockingIfNecessary, deviceState } = applications;
const messageFromError = (err?: Error | string | null): string => { const messageFromError = (err?: Error | string | null): string => {
@ -36,8 +35,9 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
const { appId } = req.params; const { appId } = req.params;
return _lockingIfNecessary(appId, { force }, () => { return _lockingIfNecessary(appId, { force }, () => {
return applications.getCurrentApp(appId) return applications
.then((app) => { .getCurrentApp(appId)
.then(app => {
if (app == null) { if (app == null) {
res.status(404).send(appNotFoundMessage); res.status(404).send(appNotFoundMessage);
return; return;
@ -47,90 +47,100 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
res.status(404).send(serviceNotFoundMessage); res.status(404).send(serviceNotFoundMessage);
return; return;
} }
applications.setTargetVolatileForService( applications.setTargetVolatileForService(service.imageId!, {
service.imageId!, running: action !== 'stop',
{ running: action !== 'stop' }, });
); return applications
return applications.executeStepAction( .executeStepAction(
serviceAction( serviceAction(action, service.serviceId!, service, service, {
action, wait: true,
service.serviceId!, }),
service, { skipLock: true },
service, )
{ wait: true },
),
{ skipLock: true },
)
.then(() => { .then(() => {
res.status(200).send('OK'); res.status(200).send('OK');
}); });
}) })
.catch((err) => { .catch(err => {
res.status(503).send(messageFromError(err)); res.status(503).send(messageFromError(err));
}); });
}); });
}; };
router.post('/v2/applications/:appId/purge', (req: Request, res: Response) => { router.post(
const { force } = req.body; '/v2/applications/:appId/purge',
const { appId } = req.params; (req: Request, res: Response) => {
const { force } = req.body;
const { appId } = req.params;
return doPurge(applications, appId, force) return doPurge(applications, appId, force)
.then(() => { .then(() => {
res.status(200).send('OK'); res.status(200).send('OK');
}) })
.catch((err) => { .catch(err => {
let message; let message;
if (err != null) { if (err != null) {
message = err.message; message = err.message;
if (message == null) { if (message == null) {
message = err; message = err;
}
} else {
message = 'Unknown error';
} }
} else { res.status(503).send(message);
message = 'Unknown error'; });
} },
res.status(503).send(message); );
});
});
router.post('/v2/applications/:appId/restart-service', (req: Request, res: Response) => { router.post(
return handleServiceAction(req, res, 'restart'); '/v2/applications/:appId/restart-service',
}); (req: Request, res: Response) => {
return handleServiceAction(req, res, 'restart');
},
);
router.post('/v2/applications/:appId/stop-service', (req: Request, res: Response) => { router.post(
return handleServiceAction(req, res, 'stop'); '/v2/applications/:appId/stop-service',
}); (req: Request, res: Response) => {
return handleServiceAction(req, res, 'stop');
},
);
router.post('/v2/applications/:appId/start-service', (req: Request, res: Response) => { router.post(
return handleServiceAction(req, res, 'start'); '/v2/applications/:appId/start-service',
}); (req: Request, res: Response) => {
return handleServiceAction(req, res, 'start');
},
);
router.post('/v2/applications/:appId/restart', (req: Request, res: Response) => { router.post(
const { force } = req.body; '/v2/applications/:appId/restart',
const { appId } = req.params; (req: Request, res: Response) => {
const { force } = req.body;
const { appId } = req.params;
return doRestart(applications, appId, force) return doRestart(applications, appId, force)
.then(() => { .then(() => {
res.status(200).send('OK'); res.status(200).send('OK');
}) })
.catch((err) => { .catch(err => {
res.status(503).send(messageFromError(err)); res.status(503).send(messageFromError(err));
}); });
}); },
);
// TODO: Support dependent applications when this feature is complete // TODO: Support dependent applications when this feature is complete
router.get('/v2/applications/state', (_req: Request, res: Response) => { router.get('/v2/applications/state', (_req: Request, res: Response) => {
// It's kinda hacky to access the services and db via the application manager // It's kinda hacky to access the services and db via the application manager
// maybe refactor this code // maybe refactor this code
Bluebird.join( Bluebird.join(
applications.services.getStatus(), applications.services.getStatus(),
applications.images.getStatus(), applications.images.getStatus(),
applications.db.models('app').select([ 'appId', 'commit', 'name' ]), applications.db.models('app').select(['appId', 'commit', 'name']),
( (
services, services,
images, images,
apps: Array<{ appId: string, commit: string, name: string }>, apps: Array<{ appId: string; commit: string; name: string }>,
) => { ) => {
// Create an object which is keyed my application name // Create an object which is keyed my application name
const response: { const response: {
@ -142,25 +152,25 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
status: string; status: string;
releaseId: number; releaseId: number;
downloadProgress: number | null; downloadProgress: number | null;
} };
} };
} };
} = { }; } = {};
const appNameById: { [id: number]: string } = { }; const appNameById: { [id: number]: string } = {};
apps.forEach((app) => { apps.forEach(app => {
const appId = parseInt(app.appId, 10); const appId = parseInt(app.appId, 10);
response[app.name] = { response[app.name] = {
appId, appId,
commit: app.commit, commit: app.commit,
services: { }, services: {},
}; };
appNameById[appId] = app.name; appNameById[appId] = app.name;
}); });
images.forEach((img) => { images.forEach(img => {
const appName = appNameById[img.appId]; const appName = appNameById[img.appId];
if (appName == null) { if (appName == null) {
console.log('Image found for unknown application!'); console.log('Image found for unknown application!');
@ -186,16 +196,19 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
}); });
res.status(200).json(response); res.status(200).json(response);
}); },
);
}); });
router.get('/v2/applications/:appId/state', (_req: Request, res: Response) => { router.get(
// Get all services and their statuses, and return it '/v2/applications/:appId/state',
applications.getStatus() (_req: Request, res: Response) => {
.then((apps) => { // Get all services and their statuses, and return it
applications.getStatus().then(apps => {
res.status(200).json(apps); res.status(200).json(apps);
}); });
}); },
);
router.get('/v2/local/target-state', async (_req, res) => { router.get('/v2/local/target-state', async (_req, res) => {
try { try {
@ -249,7 +262,6 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
message: e.message, message: e.message,
}); });
} }
} catch (e) { } catch (e) {
const message = 'Could not apply target state: '; const message = 'Could not apply target state: ';
res.status(503).json({ res.status(503).json({
@ -263,13 +275,14 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
// Return the device type and slug so that local mode builds can use this to // Return the device type and slug so that local mode builds can use this to
// resolve builds // resolve builds
try { try {
// FIXME: We should be mounting the following file into the supervisor from the // FIXME: We should be mounting the following file into the supervisor from the
// start-resin-supervisor script, changed in meta-resin - but until then, hardcode it // start-resin-supervisor script, changed in meta-resin - but until then, hardcode it
const data = await fs.readFile('/mnt/root/resin-boot/device-type.json', 'utf8'); const data = await fs.readFile(
'/mnt/root/resin-boot/device-type.json',
'utf8',
);
const deviceInfo = JSON.parse(data); const deviceInfo = JSON.parse(data);
return res.status(200).json({ return res.status(200).json({
status: 'sucess', status: 'sucess',
info: { info: {
@ -277,7 +290,6 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
deviceType: deviceInfo.slug, deviceType: deviceInfo.slug,
}, },
}); });
} catch (e) { } catch (e) {
const message = 'Could not fetch device information: '; const message = 'Could not fetch device information: ';
res.status(503).json({ res.status(503).json({
@ -289,7 +301,9 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
router.get('/v2/local/logs', async (_req, res) => { router.get('/v2/local/logs', async (_req, res) => {
const backend = applications.logger.getLocalBackend(); const backend = applications.logger.getLocalBackend();
backend.assignServiceNameResolver(applications.serviceNameFromId.bind(applications)); backend.assignServiceNameResolver(
applications.serviceNameFromId.bind(applications),
);
// Get the stream, and stream it into res // Get the stream, and stream it into res
const listenStream = backend.attachListener(); const listenStream = backend.attachListener();

View File

@ -258,7 +258,6 @@ module.exports = class DeviceState extends EventEmitter
@config.get('apiEndpoint'), @config.get('apiEndpoint'),
validateState(target), validateState(target),
(apiEndpoint) => (apiEndpoint) =>
source = apiEndpoint
@usingWriteLockTarget => @usingWriteLockTarget =>
# Apps, deviceConfig, dependent # Apps, deviceConfig, dependent
@db.transaction (trx) => @db.transaction (trx) =>
@ -318,9 +317,9 @@ module.exports = class DeviceState extends EventEmitter
_.assign(@_currentVolatile, newState) _.assign(@_currentVolatile, newState)
@emitAsync('change') @emitAsync('change')
_convertLegacyAppsJson: (appsArray) => _convertLegacyAppsJson: (appsArray) ->
Promise.try => Promise.try ->
deviceConf = _.reduce(appsArray, (conf, app) => deviceConf = _.reduce(appsArray, (conf, app) ->
return _.merge({}, conf, app.config) return _.merge({}, conf, app.config)
, {}) , {})
apps = _.keyBy(_.map(appsArray, singleToMulticontainerApp), 'appId') apps = _.keyBy(_.map(appsArray, singleToMulticontainerApp), 'appId')
@ -333,9 +332,9 @@ module.exports = class DeviceState extends EventEmitter
.then(JSON.parse) .then(JSON.parse)
.then (stateFromFile) => .then (stateFromFile) =>
if _.isArray(stateFromFile) if _.isArray(stateFromFile)
# This is a legacy apps.json # This is a legacy apps.json
console.log('Legacy apps.json detected') console.log('Legacy apps.json detected')
return @_convertLegacyAppsJson(stateFromFile) return @_convertLegacyAppsJson(stateFromFile)
else else
return stateFromFile return stateFromFile
.then (stateFromFile) => .then (stateFromFile) =>

View File

@ -32,7 +32,6 @@ const mixpanelMask = [
].join(','); ].join(',');
export class EventTracker { export class EventTracker {
private defaultProperties: EventTrackProperties | null; private defaultProperties: EventTrackProperties | null;
private client: any; private client: any;
@ -60,11 +59,7 @@ export class EventTracker {
}); });
} }
public track( public track(event: string, properties: EventTrackProperties | Error = {}) {
event: string,
properties: EventTrackProperties | Error = { },
) {
if (properties instanceof Error) { if (properties instanceof Error) {
properties = { error: properties }; properties = { error: properties };
} }
@ -89,12 +84,19 @@ export class EventTracker {
this.throttleddLogger(event)(properties); this.throttleddLogger(event)(properties);
} }
private throttleddLogger = memoizee((event: string) => { private throttleddLogger = memoizee(
// Call this function at maximum once every minute (event: string) => {
return _.throttle((properties) => { // Call this function at maximum once every minute
this.client.track(event, properties); return _.throttle(
}, eventDebounceTime, { leading: true }); properties => {
}, { primitive: true }); this.client.track(event, properties);
},
eventDebounceTime,
{ leading: true },
);
},
{ primitive: true },
);
private logEvent(...args: string[]) { private logEvent(...args: string[]) {
console.log(...args); console.log(...args);
@ -103,6 +105,6 @@ export class EventTracker {
private assignDefaultProperties( private assignDefaultProperties(
properties: EventTrackProperties, properties: EventTrackProperties,
): EventTrackProperties { ): EventTrackProperties {
return _.merge({ }, properties, this.defaultProperties); return _.merge({}, properties, this.defaultProperties);
} }
} }

View File

@ -7,14 +7,19 @@ const supervisorNetworkInterface = 'supervisor0';
const constants = { const constants = {
rootMountPoint, rootMountPoint,
databasePath: checkString(process.env.DATABASE_PATH) || '/data/database.sqlite', databasePath:
checkString(process.env.DATABASE_PATH) || '/data/database.sqlite',
dockerSocket: process.env.DOCKER_SOCKET || '/var/run/docker.sock', dockerSocket: process.env.DOCKER_SOCKET || '/var/run/docker.sock',
supervisorImage: checkString(process.env.SUPERVISOR_IMAGE) || 'resin/rpi-supervisor', supervisorImage:
ledFile: checkString(process.env.LED_FILE) || '/sys/class/leds/led0/brightness', checkString(process.env.SUPERVISOR_IMAGE) || 'resin/rpi-supervisor',
ledFile:
checkString(process.env.LED_FILE) || '/sys/class/leds/led0/brightness',
vpnStatusPath: vpnStatusPath:
checkString(process.env.VPN_STATUS_PATH) || `${rootMountPoint}/run/openvpn/vpn_status`, checkString(process.env.VPN_STATUS_PATH) ||
`${rootMountPoint}/run/openvpn/vpn_status`,
hostOSVersionPath: hostOSVersionPath:
checkString(process.env.HOST_OS_VERSION_PATH) || `${rootMountPoint}/etc/os-release`, checkString(process.env.HOST_OS_VERSION_PATH) ||
`${rootMountPoint}/etc/os-release`,
privateAppEnvVars: [ privateAppEnvVars: [
'RESIN_SUPERVISOR_API_KEY', 'RESIN_SUPERVISOR_API_KEY',
'RESIN_API_KEY', 'RESIN_API_KEY',
@ -28,7 +33,13 @@ const constants = {
configJsonNonAtomicPath: '/boot/config.json', configJsonNonAtomicPath: '/boot/config.json',
defaultMixpanelToken: process.env.DEFAULT_MIXPANEL_TOKEN, defaultMixpanelToken: process.env.DEFAULT_MIXPANEL_TOKEN,
supervisorNetworkInterface: supervisorNetworkInterface, supervisorNetworkInterface: supervisorNetworkInterface,
allowedInterfaces: [ 'resin-vpn', 'tun0', 'docker0', 'lo', supervisorNetworkInterface ], allowedInterfaces: [
'resin-vpn',
'tun0',
'docker0',
'lo',
supervisorNetworkInterface,
],
appsJsonPath: process.env.APPS_JSON_PATH || '/boot/apps.json', appsJsonPath: process.env.APPS_JSON_PATH || '/boot/apps.json',
ipAddressUpdateInterval: 30 * 1000, ipAddressUpdateInterval: 30 * 1000,
imageCleanupErrorIgnoreTimeout: 3600 * 1000, imageCleanupErrorIgnoreTimeout: 3600 * 1000,

View File

@ -6,8 +6,10 @@ export function envArrayToObject(env: string[]): EnvVarObject {
const toPair = (keyVal: string) => { const toPair = (keyVal: string) => {
const m = keyVal.match(/^([^=]+)=\s*(.*)\s*$/); const m = keyVal.match(/^([^=]+)=\s*(.*)\s*$/);
if (m == null) { if (m == null) {
console.log(`WARNING: Could not correctly parse env var ${keyVal}. ` + console.log(
'Please fix this var and recreate the container.'); `WARNING: Could not correctly parse env var ${keyVal}. ` +
'Please fix this var and recreate the container.',
);
return [null, null]; return [null, null];
} }
return m.slice(1); return m.slice(1);

View File

@ -78,7 +78,7 @@ module.exports = class DockerUtils extends DockerToolbelt
# Since the supervisor never calls this function without a source anymore, # Since the supervisor never calls this function without a source anymore,
# this should never happen, but we handle it anyways. # this should never happen, but we handle it anyways.
if !deltaSource? if !deltaSource?
log("Falling back to regular pull due to lack of a delta source") log('Falling back to regular pull due to lack of a delta source')
return @fetchImageWithProgress(imgDest, fullDeltaOpts, onProgress) return @fetchImageWithProgress(imgDest, fullDeltaOpts, onProgress)
docker = this docker = this

View File

@ -19,17 +19,20 @@ declare class DockerUtils extends DockerToolbelt {
getRepoAndTag(image: string): Bluebird<TaggedRepoImage>; getRepoAndTag(image: string): Bluebird<TaggedRepoImage>;
fetchDeltaWithProgress(imgDest: string, fullDeltaOpts: any, onProgress: (args: any) => void): Bluebird<void>; fetchDeltaWithProgress(
imgDest: string,
fullDeltaOpts: any,
onProgress: (args: any) => void,
): Bluebird<void>;
fetchImageWithProgress( fetchImageWithProgress(
image: string, image: string,
config: { uuid: string, currentApiKey: string }, config: { uuid: string; currentApiKey: string },
onProgress: (args: any) => void, onProgress: (args: any) => void,
): Bluebird<void>; ): Bluebird<void>;
getImageEnv(id: string): Bluebird<EnvVarObject>; getImageEnv(id: string): Bluebird<EnvVarObject>;
getNetworkGateway(netName: string): Bluebird<string>; getNetworkGateway(netName: string): Bluebird<string>;
} }
export = DockerUtils; export = DockerUtils;

View File

@ -29,9 +29,9 @@ export function UnitNotLoadedError(err: string[]): boolean {
return endsWith(err[0], 'not loaded.'); return endsWith(err[0], 'not loaded.');
} }
export class InvalidNetGatewayError extends TypedError { } export class InvalidNetGatewayError extends TypedError {}
export class DeltaStillProcessingError extends TypedError { } export class DeltaStillProcessingError extends TypedError {}
export class InvalidAppIdError extends TypedError { export class InvalidAppIdError extends TypedError {
public constructor(public appId: any) { public constructor(public appId: any) {

View File

@ -3,17 +3,17 @@ import { fs } from 'mz';
import * as path from 'path'; import * as path from 'path';
export function writeAndSyncFile(path: string, data: string): Bluebird<void> { export function writeAndSyncFile(path: string, data: string): Bluebird<void> {
return Bluebird.resolve(fs.open(path, 'w')) return Bluebird.resolve(fs.open(path, 'w')).then(fd => {
.then((fd) => { fs.write(fd, data, 0, 'utf8')
fs.write(fd, data, 0, 'utf8') .then(() => fs.fsync(fd))
.then(() => fs.fsync(fd)) .then(() => fs.close(fd));
.then(() => fs.close(fd)); });
});
} }
export function writeFileAtomic(path: string, data: string): Bluebird<void> { export function writeFileAtomic(path: string, data: string): Bluebird<void> {
return Bluebird.resolve(writeAndSyncFile(`${path}.new`, data)) return Bluebird.resolve(writeAndSyncFile(`${path}.new`, data)).then(() =>
.then(() => fs.rename(`${path}.new`, path)); fs.rename(`${path}.new`, path),
);
} }
export function safeRename(src: string, dest: string): Bluebird<void> { export function safeRename(src: string, dest: string): Bluebird<void> {

View File

@ -28,11 +28,21 @@ export function rejectOnAllInterfacesExcept(
): Promise<void> { ): Promise<void> {
// We delete each rule and create it again to ensure ordering (all ACCEPTs before the REJECT/DROP). // We delete each rule and create it again to ensure ordering (all ACCEPTs before the REJECT/DROP).
// This is especially important after a supervisor update. // This is especially important after a supervisor update.
return Promise.each(allowedInterfaces, (iface) => clearAndInsertIptablesRule(`INPUT -p tcp --dport ${port} -i ${iface} -j ACCEPT`)) return (
.then(() => clearAndAppendIptablesRule(`INPUT -p tcp --dport ${port} -j REJECT`)) Promise.each(allowedInterfaces, iface =>
// On systems without REJECT support, fall back to DROP clearAndInsertIptablesRule(
.catch(() => clearAndAppendIptablesRule(`INPUT -p tcp --dport ${port} -j DROP`)) `INPUT -p tcp --dport ${port} -i ${iface} -j ACCEPT`,
.return(); ),
)
.then(() =>
clearAndAppendIptablesRule(`INPUT -p tcp --dport ${port} -j REJECT`),
)
// On systems without REJECT support, fall back to DROP
.catch(() =>
clearAndAppendIptablesRule(`INPUT -p tcp --dport ${port} -j DROP`),
)
.return()
);
} }
export function removeRejections(port: number): Promise<void> { export function removeRejections(port: number): Promise<void> {

View File

@ -11,164 +11,164 @@ export const stopServiceSuccess: LogType = {
eventName: 'Service stop', eventName: 'Service stop',
humanName: 'Killed service', humanName: 'Killed service',
}; };
export const stopServiceNoop: LogType = { export const stopServiceNoop: LogType = {
eventName: 'Service already stopped', eventName: 'Service already stopped',
humanName: 'Service is already stopped, removing container', humanName: 'Service is already stopped, removing container',
}; };
export const stopRemoveServiceNoop: LogType = { export const stopRemoveServiceNoop: LogType = {
eventName: 'Service already stopped and container removed', eventName: 'Service already stopped and container removed',
humanName: 'Service is already stopped and the container removed', humanName: 'Service is already stopped and the container removed',
}; };
export const stopServiceError: LogType = { export const stopServiceError: LogType = {
eventName: 'Service stop error', eventName: 'Service stop error',
humanName: 'Failed to kill service', humanName: 'Failed to kill service',
}; };
export const removeDeadService: LogType = { export const removeDeadService: LogType = {
eventName: 'Remove dead container', eventName: 'Remove dead container',
humanName: 'Removing dead container', humanName: 'Removing dead container',
}; };
export const removeDeadServiceError: LogType = { export const removeDeadServiceError: LogType = {
eventName: 'Remove dead container error', eventName: 'Remove dead container error',
humanName: 'Error removing dead container', humanName: 'Error removing dead container',
}; };
export const downloadImage: LogType = { export const downloadImage: LogType = {
eventName: 'Docker image download', eventName: 'Docker image download',
humanName: 'Downloading image', humanName: 'Downloading image',
}; };
export const downloadImageDelta: LogType = { export const downloadImageDelta: LogType = {
eventName: 'Delta image download', eventName: 'Delta image download',
humanName: 'Downloading delta for image', humanName: 'Downloading delta for image',
}; };
export const downloadImageSuccess: LogType = { export const downloadImageSuccess: LogType = {
eventName: 'Image downloaded', eventName: 'Image downloaded',
humanName: 'Downloaded image', humanName: 'Downloaded image',
}; };
export const downloadImageError: LogType = { export const downloadImageError: LogType = {
eventName: 'Image download error', eventName: 'Image download error',
humanName: 'Failed to download image', humanName: 'Failed to download image',
}; };
export const installService: LogType = { export const installService: LogType = {
eventName: 'Service install', eventName: 'Service install',
humanName: 'Installing service', humanName: 'Installing service',
}; };
export const installServiceSuccess: LogType = { export const installServiceSuccess: LogType = {
eventName: 'Service installed', eventName: 'Service installed',
humanName: 'Installed service', humanName: 'Installed service',
}; };
export const installServiceError: LogType = { export const installServiceError: LogType = {
eventName: 'Service install error', eventName: 'Service install error',
humanName: 'Failed to install service', humanName: 'Failed to install service',
}; };
export const deleteImage: LogType = { export const deleteImage: LogType = {
eventName: 'Image removal', eventName: 'Image removal',
humanName: 'Deleting image', humanName: 'Deleting image',
}; };
export const deleteImageSuccess: LogType = { export const deleteImageSuccess: LogType = {
eventName: 'Image removed', eventName: 'Image removed',
humanName: 'Deleted image', humanName: 'Deleted image',
}; };
export const deleteImageError: LogType = { export const deleteImageError: LogType = {
eventName: 'Image removal error', eventName: 'Image removal error',
humanName: 'Failed to delete image', humanName: 'Failed to delete image',
}; };
export const imageAlreadyDeleted: LogType = { export const imageAlreadyDeleted: LogType = {
eventName: 'Image already deleted', eventName: 'Image already deleted',
humanName: 'Image already deleted', humanName: 'Image already deleted',
}; };
export const deltaStillProcessingError: LogType = { export const deltaStillProcessingError: LogType = {
eventName: 'Delta still processing remotely.', eventName: 'Delta still processing remotely.',
humanName: 'Delta still processing remotely. Will retry...', humanName: 'Delta still processing remotely. Will retry...',
}; };
export const startService: LogType = { export const startService: LogType = {
eventName: 'Service start', eventName: 'Service start',
humanName: 'Starting service', humanName: 'Starting service',
}; };
export const startServiceSuccess: LogType = { export const startServiceSuccess: LogType = {
eventName: 'Service started', eventName: 'Service started',
humanName: 'Started service', humanName: 'Started service',
}; };
export const startServiceNoop: LogType = { export const startServiceNoop: LogType = {
eventName: 'Service already running', eventName: 'Service already running',
humanName: 'Service is already running', humanName: 'Service is already running',
}; };
export const startServiceError: LogType = { export const startServiceError: LogType = {
eventName: 'Service start error', eventName: 'Service start error',
humanName: 'Failed to start service', humanName: 'Failed to start service',
}; };
export const updateService: LogType = { export const updateService: LogType = {
eventName: 'Service update', eventName: 'Service update',
humanName: 'Updating service', humanName: 'Updating service',
}; };
export const updateServiceError: LogType = { export const updateServiceError: LogType = {
eventName: 'Service update error', eventName: 'Service update error',
humanName: 'Failed to update service', humanName: 'Failed to update service',
}; };
export const serviceExit: LogType = { export const serviceExit: LogType = {
eventName: 'Service exit', eventName: 'Service exit',
humanName: 'Service exited', humanName: 'Service exited',
}; };
export const serviceRestart: LogType = { export const serviceRestart: LogType = {
eventName: 'Service restart', eventName: 'Service restart',
humanName: 'Restarting service', humanName: 'Restarting service',
}; };
export const updateServiceConfig: LogType = { export const updateServiceConfig: LogType = {
eventName: 'Service config update', eventName: 'Service config update',
humanName: 'Updating config for service', humanName: 'Updating config for service',
}; };
export const updateServiceConfigSuccess: LogType = { export const updateServiceConfigSuccess: LogType = {
eventName: 'Service config updated', eventName: 'Service config updated',
humanName: 'Updated config for service', humanName: 'Updated config for service',
}; };
export const updateServiceConfigError: LogType = { export const updateServiceConfigError: LogType = {
eventName: 'Service config update error', eventName: 'Service config update error',
humanName: 'Failed to update config for service', humanName: 'Failed to update config for service',
}; };
export const createVolume: LogType = { export const createVolume: LogType = {
eventName: 'Volume creation', eventName: 'Volume creation',
humanName: 'Creating volume', humanName: 'Creating volume',
}; };
export const createVolumeError: LogType = { export const createVolumeError: LogType = {
eventName: 'Volume creation error', eventName: 'Volume creation error',
humanName: 'Error creating volume', humanName: 'Error creating volume',
}; };
export const removeVolume: LogType = { export const removeVolume: LogType = {
eventName: 'Volume removal', eventName: 'Volume removal',
humanName: 'Removing volume', humanName: 'Removing volume',
}; };
export const removeVolumeError: LogType = { export const removeVolumeError: LogType = {
eventName: 'Volume removal error', eventName: 'Volume removal error',
humanName: 'Error removing volume', humanName: 'Error removing volume',
}; };
export const createNetwork: LogType = { export const createNetwork: LogType = {
eventName: 'Network creation', eventName: 'Network creation',
humanName: 'Creating network', humanName: 'Creating network',
}; };
export const createNetworkError: LogType = { export const createNetworkError: LogType = {
eventName: 'Network creation error', eventName: 'Network creation error',
humanName: 'Error creating network', humanName: 'Error creating network',
}; };
export const removeNetwork: LogType = { export const removeNetwork: LogType = {
eventName: 'Network removal', eventName: 'Network removal',
humanName: 'Removing network', humanName: 'Removing network',
}; };
export const removeNetworkError: LogType = { export const removeNetworkError: LogType = {
eventName: 'Network removal error', eventName: 'Network removal error',
humanName: 'Error removing network', humanName: 'Error removing network',
}; };

View File

@ -2,4 +2,5 @@ export const appNotFoundMessage = `App not found: an app needs to be installed f
If you've recently moved this device from another app, If you've recently moved this device from another app,
please push an app and wait for it to be installed first.`; please push an app and wait for it to be installed first.`;
export const serviceNotFoundMessage = 'Service not found, a container must exist for this endpoint to work'; export const serviceNotFoundMessage =
'Service not found, a container must exist for this endpoint to work';

View File

@ -7,10 +7,10 @@ function getOSReleaseField(path: string, field: string): string | undefined {
try { try {
const releaseData = fs.readFileSync(path, 'utf-8'); const releaseData = fs.readFileSync(path, 'utf-8');
const lines = releaseData.split('\n'); const lines = releaseData.split('\n');
const releaseItems: { [field: string]: string} = { }; const releaseItems: { [field: string]: string } = {};
for (const line of lines) { for (const line of lines) {
const [ key, value ] = line.split('='); const [key, value] = line.split('=');
releaseItems[_.trim(key)] = _.trim(value); releaseItems[_.trim(key)] = _.trim(value);
} }
@ -20,7 +20,7 @@ function getOSReleaseField(path: string, field: string): string | undefined {
// Remove enclosing quotes: http://stackoverflow.com/a/19156197/2549019 // Remove enclosing quotes: http://stackoverflow.com/a/19156197/2549019
return releaseItems[field].replace(/^"(.+(?="$))"$/, '$1'); return releaseItems[field].replace(/^"(.+(?="$))"$/, '$1');
} catch(err) { } catch (err) {
console.log('Could not get OS release field: ', err.message); console.log('Could not get OS release field: ', err.message);
return; return;
} }

View File

@ -16,9 +16,9 @@ export interface ConfigMap {
export interface ConfigSchema { export interface ConfigSchema {
[key: string]: { [key: string]: {
source: string, source: string;
default?: any, default?: any;
mutable?: boolean, mutable?: boolean;
removeIfNull?: boolean, removeIfNull?: boolean;
}; };
} }

View File

@ -4,8 +4,7 @@ export interface LockCallback {
(appId: number, opts: { force: boolean }, fn: () => void): Promise<void>; (appId: number, opts: { force: boolean }, fn: () => void): Promise<void>;
} }
export class UpdatesLockedError extends TypedError { export class UpdatesLockedError extends TypedError {}
}
export function lock(): LockCallback; export function lock(): LockCallback;
export function lockPath(appId: number, serviceName: string): string; export function lockPath(appId: number, serviceName: string): string;

View File

@ -19,7 +19,10 @@ type NullableLiteral = number | NullableString;
* Check an input string as a number, optionally specifying a requirement * Check an input string as a number, optionally specifying a requirement
* to be positive * to be positive
*/ */
export function checkInt(s: NullableLiteral, options: CheckIntOptions = {}): number | void { export function checkInt(
s: NullableLiteral,
options: CheckIntOptions = {},
): number | void {
if (s == null) { if (s == null) {
return; return;
} }
@ -45,7 +48,7 @@ export function checkInt(s: NullableLiteral, options: CheckIntOptions = {}): num
* Check that a string exists, and is not an empty string, 'null', or 'undefined' * Check that a string exists, and is not an empty string, 'null', or 'undefined'
*/ */
export function checkString(s: NullableLiteral): string | void { export function checkString(s: NullableLiteral): string | void {
if (s == null || !_.isString(s) || _.includes([ 'null', 'undefined', '' ], s)) { if (s == null || !_.isString(s) || _.includes(['null', 'undefined', ''], s)) {
return; return;
} }
@ -59,7 +62,7 @@ export function checkString(s: NullableLiteral): string | void {
* which represents if the input was truthy * which represents if the input was truthy
*/ */
export function checkTruthy(v: string | boolean | number): boolean | void { export function checkTruthy(v: string | boolean | number): boolean | void {
switch(v) { switch (v) {
case '1': case '1':
case 'true': case 'true':
case true: case true:
@ -102,7 +105,9 @@ export function isValidEnv(obj: EnvVarObject): boolean {
return _.every(obj, (val, key) => { return _.every(obj, (val, key) => {
if (!isValidShortText(key)) { if (!isValidShortText(key)) {
console.log('debug: Non-valid short text env var key passed to validation.isValidEnv'); console.log(
'debug: Non-valid short text env var key passed to validation.isValidEnv',
);
console.log(`\tKey: ${inspect(key)}`); console.log(`\tKey: ${inspect(key)}`);
return false; return false;
} }
@ -136,19 +141,25 @@ export function isValidLabelsObject(obj: LabelObject): boolean {
return _.every(obj, (val, key) => { return _.every(obj, (val, key) => {
if (!isValidShortText(key)) { if (!isValidShortText(key)) {
console.log('debug: Non-valid short text label key passed to validation.isValidLabelsObject'); console.log(
'debug: Non-valid short text label key passed to validation.isValidLabelsObject',
);
console.log(`\tkey: ${inspect(key)}`); console.log(`\tkey: ${inspect(key)}`);
return false; return false;
} }
if (!LABEL_NAME_REGEX.test(key)) { if (!LABEL_NAME_REGEX.test(key)) {
console.log('debug: Invalid label name passed to validation.isValidLabelsObject'); console.log(
'debug: Invalid label name passed to validation.isValidLabelsObject',
);
console.log(`\tkey: ${inspect(key)}`); console.log(`\tkey: ${inspect(key)}`);
return false; return false;
} }
if (!_.isString(val)) { if (!_.isString(val)) {
console.log('debug: Non-string value passed to validation.isValidLabelsObject'); console.log(
'debug: Non-string value passed to validation.isValidLabelsObject',
);
console.log(`\tval: ${inspect(val)}`); console.log(`\tval: ${inspect(val)}`);
return false; return false;
} }
@ -161,7 +172,9 @@ export function isValidDeviceName(name: string): boolean {
// currently the only disallowed value in a device name is a newline // currently the only disallowed value in a device name is a newline
const newline = name.indexOf('\n') !== -1; const newline = name.indexOf('\n') !== -1;
if (newline) { if (newline) {
console.log('debug: newline found in device name. This is invalid and should be removed'); console.log(
'debug: newline found in device name. This is invalid and should be removed',
);
} }
return !newline; return !newline;
} }
@ -179,7 +192,9 @@ function undefinedOrValidEnv(val: EnvVarObject): boolean {
*/ */
export function isValidDependentAppsObject(apps: any): boolean { export function isValidDependentAppsObject(apps: any): boolean {
if (!_.isObject(apps)) { if (!_.isObject(apps)) {
console.log('debug: non-object passed to validation.isValidDependentAppsObject'); console.log(
'debug: non-object passed to validation.isValidDependentAppsObject',
);
console.log(`\tapps: ${inspect(apps)}`); console.log(`\tapps: ${inspect(apps)}`);
return false; return false;
} }
@ -193,7 +208,9 @@ export function isValidDependentAppsObject(apps: any): boolean {
}); });
if (!isValidShortText(appId) || !checkInt(appId)) { if (!isValidShortText(appId) || !checkInt(appId)) {
console.log('debug: Invalid appId passed to validation.isValidDependentAppsObject'); console.log(
'debug: Invalid appId passed to validation.isValidDependentAppsObject',
);
console.log(`\tappId: ${inspect(appId)}`); console.log(`\tappId: ${inspect(appId)}`);
return false; return false;
} }
@ -201,7 +218,9 @@ export function isValidDependentAppsObject(apps: any): boolean {
return _.conformsTo(val, { return _.conformsTo(val, {
name: (n: any) => { name: (n: any) => {
if (!isValidShortText(n)) { if (!isValidShortText(n)) {
console.log('debug: Invalid name passed to validation.isValidDependentAppsObject'); console.log(
'debug: Invalid name passed to validation.isValidDependentAppsObject',
);
console.log(`\tname: ${inspect(n)}`); console.log(`\tname: ${inspect(n)}`);
return false; return false;
} }
@ -209,7 +228,9 @@ export function isValidDependentAppsObject(apps: any): boolean {
}, },
image: (i: any) => { image: (i: any) => {
if (val.commit != null && !isValidShortText(i)) { if (val.commit != null && !isValidShortText(i)) {
console.log('debug: non valid image passed to validation.isValidDependentAppsObject'); console.log(
'debug: non valid image passed to validation.isValidDependentAppsObject',
);
console.log(`\timage: ${inspect(i)}`); console.log(`\timage: ${inspect(i)}`);
return false; return false;
} }
@ -217,7 +238,9 @@ export function isValidDependentAppsObject(apps: any): boolean {
}, },
commit: (c: any) => { commit: (c: any) => {
if (c != null && !isValidShortText(c)) { if (c != null && !isValidShortText(c)) {
console.log('debug: invalid commit passed to validation.isValidDependentAppsObject'); console.log(
'debug: invalid commit passed to validation.isValidDependentAppsObject',
);
console.log(`\tcommit: ${inspect(c)}`); console.log(`\tcommit: ${inspect(c)}`);
return false; return false;
} }
@ -225,7 +248,9 @@ export function isValidDependentAppsObject(apps: any): boolean {
}, },
config: (c: any) => { config: (c: any) => {
if (!undefinedOrValidEnv(c)) { if (!undefinedOrValidEnv(c)) {
console.log('debug; Invalid config passed to validation.isValidDependentAppsObject'); console.log(
'debug; Invalid config passed to validation.isValidDependentAppsObject',
);
console.log(`\tconfig: ${inspect(c)}`); console.log(`\tconfig: ${inspect(c)}`);
return false; return false;
} }
@ -233,7 +258,9 @@ export function isValidDependentAppsObject(apps: any): boolean {
}, },
environment: (e: any) => { environment: (e: any) => {
if (!undefinedOrValidEnv(e)) { if (!undefinedOrValidEnv(e)) {
console.log('debug; Invalid environment passed to validation.isValidDependentAppsObject'); console.log(
'debug; Invalid environment passed to validation.isValidDependentAppsObject',
);
console.log(`\tenvironment: ${inspect(e)}`); console.log(`\tenvironment: ${inspect(e)}`);
return false; return false;
} }
@ -245,7 +272,9 @@ export function isValidDependentAppsObject(apps: any): boolean {
function isValidService(service: any, serviceId: string): boolean { function isValidService(service: any, serviceId: string): boolean {
if (!isValidShortText(serviceId) || !checkInt(serviceId)) { if (!isValidShortText(serviceId) || !checkInt(serviceId)) {
console.log('debug: Invalid service id passed to validation.isValidService'); console.log(
'debug: Invalid service id passed to validation.isValidService',
);
console.log(`\tserviceId: ${inspect(serviceId)}`); console.log(`\tserviceId: ${inspect(serviceId)}`);
return false; return false;
} }
@ -253,7 +282,9 @@ function isValidService(service: any, serviceId: string): boolean {
return _.conformsTo(service, { return _.conformsTo(service, {
serviceName: (n: any) => { serviceName: (n: any) => {
if (!isValidShortText(n)) { if (!isValidShortText(n)) {
console.log('debug: Invalid service name passed to validation.isValidService'); console.log(
'debug: Invalid service name passed to validation.isValidService',
);
console.log(`\tserviceName: ${inspect(n)}`); console.log(`\tserviceName: ${inspect(n)}`);
return false; return false;
} }
@ -277,7 +308,9 @@ function isValidService(service: any, serviceId: string): boolean {
}, },
imageId: (i: any) => { imageId: (i: any) => {
if (checkInt(i) == null) { if (checkInt(i) == null) {
console.log('debug: Invalid image id passed to validation.isValidService'); console.log(
'debug: Invalid image id passed to validation.isValidService',
);
console.log(`\timageId: ${inspect(i)}`); console.log(`\timageId: ${inspect(i)}`);
return false; return false;
} }
@ -285,7 +318,9 @@ function isValidService(service: any, serviceId: string): boolean {
}, },
labels: (l: any) => { labels: (l: any) => {
if (!isValidLabelsObject(l)) { if (!isValidLabelsObject(l)) {
console.log('debug: Invalid labels object passed to validation.isValidService'); console.log(
'debug: Invalid labels object passed to validation.isValidService',
);
console.log(`\tlabels: ${inspect(l)}`); console.log(`\tlabels: ${inspect(l)}`);
return false; return false;
} }
@ -311,7 +346,9 @@ export function isValidAppsObject(obj: any): boolean {
return _.every(obj, (val, appId) => { return _.every(obj, (val, appId) => {
if (!isValidShortText(appId) || !checkInt(appId)) { if (!isValidShortText(appId) || !checkInt(appId)) {
console.log('debug: Invalid appId passed to validation.isValidAppsObject'); console.log(
'debug: Invalid appId passed to validation.isValidAppsObject',
);
console.log(`\tappId: ${inspect(appId)}`); console.log(`\tappId: ${inspect(appId)}`);
return false; return false;
} }
@ -319,7 +356,9 @@ export function isValidAppsObject(obj: any): boolean {
return _.conformsTo(_.defaults(_.clone(val), { releaseId: undefined }), { return _.conformsTo(_.defaults(_.clone(val), { releaseId: undefined }), {
name: (n: any) => { name: (n: any) => {
if (!isValidShortText(n)) { if (!isValidShortText(n)) {
console.log('debug: Invalid service name passed to validation.isValidAppsObject'); console.log(
'debug: Invalid service name passed to validation.isValidAppsObject',
);
console.log(`\tname: ${inspect(n)}`); console.log(`\tname: ${inspect(n)}`);
return false; return false;
} }
@ -327,7 +366,9 @@ export function isValidAppsObject(obj: any): boolean {
}, },
releaseId: (r: any) => { releaseId: (r: any) => {
if (r != null && checkInt(r) == null) { if (r != null && checkInt(r) == null) {
console.log('debug: Invalid releaseId passed to validation.isValidAppsObject'); console.log(
'debug: Invalid releaseId passed to validation.isValidAppsObject',
);
console.log(`\treleaseId: ${inspect(r)}`); console.log(`\treleaseId: ${inspect(r)}`);
return false; return false;
} }
@ -335,14 +376,18 @@ export function isValidAppsObject(obj: any): boolean {
}, },
services: (s: any) => { services: (s: any) => {
if (!_.isObject(s)) { if (!_.isObject(s)) {
console.log('debug: Non-object service passed to validation.isValidAppsObject'); console.log(
'debug: Non-object service passed to validation.isValidAppsObject',
);
console.log(`\tservices: ${inspect(s)}`); console.log(`\tservices: ${inspect(s)}`);
return false; return false;
} }
return _.every(s, (svc, svcId) => { return _.every(s, (svc, svcId) => {
if (!isValidService(svc, svcId)) { if (!isValidService(svc, svcId)) {
console.log('debug: Invalid service object passed to validation.isValidAppsObject'); console.log(
'debug: Invalid service object passed to validation.isValidAppsObject',
);
console.log(`\tsvc: ${inspect(svc)}`); console.log(`\tsvc: ${inspect(svc)}`);
return false; return false;
} }
@ -359,17 +404,19 @@ export function isValidAppsObject(obj: any): boolean {
* Validate a dependent devices object from the state endpoint. * Validate a dependent devices object from the state endpoint.
*/ */
export function isValidDependentDevicesObject(devices: any): boolean { export function isValidDependentDevicesObject(devices: any): boolean {
if (!_.isObject(devices)) { if (!_.isObject(devices)) {
console.log('debug: Non-object passed to validation.isValidDependentDevicesObject'); console.log(
'debug: Non-object passed to validation.isValidDependentDevicesObject',
);
console.log(`\tdevices: ${inspect(devices)}`); console.log(`\tdevices: ${inspect(devices)}`);
return false; return false;
} }
return _.every(devices, (val, uuid) => { return _.every(devices, (val, uuid) => {
if (!isValidShortText(uuid)) { if (!isValidShortText(uuid)) {
console.log('debug: Invalid uuid passed to validation.isValidDependentDevicesObject'); console.log(
'debug: Invalid uuid passed to validation.isValidDependentDevicesObject',
);
console.log(`\tuuid: ${inspect(uuid)}`); console.log(`\tuuid: ${inspect(uuid)}`);
return false; return false;
} }
@ -377,7 +424,9 @@ export function isValidDependentDevicesObject(devices: any): boolean {
return _.conformsTo(val, { return _.conformsTo(val, {
name: (n: any) => { name: (n: any) => {
if (!isValidShortText(n)) { if (!isValidShortText(n)) {
console.log('debug: Invalid device name passed to validation.isValidDependentDevicesObject'); console.log(
'debug: Invalid device name passed to validation.isValidDependentDevicesObject',
);
console.log(`\tname: ${inspect(n)}`); console.log(`\tname: ${inspect(n)}`);
return false; return false;
} }
@ -385,22 +434,31 @@ export function isValidDependentDevicesObject(devices: any): boolean {
}, },
apps: (a: any) => { apps: (a: any) => {
if (!_.isObject(a)) { if (!_.isObject(a)) {
console.log('debug: Invalid apps object passed to validation.isValidDependentDevicesObject'); console.log(
'debug: Invalid apps object passed to validation.isValidDependentDevicesObject',
);
console.log(`\tapps: ${inspect(a)}`); console.log(`\tapps: ${inspect(a)}`);
return false; return false;
} }
if (_.isEmpty(a)) { if (_.isEmpty(a)) {
console.log('debug: Empty object passed to validation.isValidDependentDevicesObject'); console.log(
'debug: Empty object passed to validation.isValidDependentDevicesObject',
);
return false; return false;
} }
return _.every(a, (app) => { return _.every(a, app => {
app = _.defaults(_.clone(app), { config: undefined, environment: undefined }); app = _.defaults(_.clone(app), {
config: undefined,
environment: undefined,
});
return _.conformsTo(app, { return _.conformsTo(app, {
config: (c: any) => { config: (c: any) => {
if (!undefinedOrValidEnv(c)) { if (!undefinedOrValidEnv(c)) {
console.log('debug: Invalid config passed to validation.isValidDependentDevicesObject'); console.log(
'debug: Invalid config passed to validation.isValidDependentDevicesObject',
);
console.log(`\tconfig: ${inspect(c)}`); console.log(`\tconfig: ${inspect(c)}`);
return false; return false;
} }
@ -408,7 +466,9 @@ export function isValidDependentDevicesObject(devices: any): boolean {
}, },
environment: (e: any) => { environment: (e: any) => {
if (!undefinedOrValidEnv(e)) { if (!undefinedOrValidEnv(e)) {
console.log('debug: Invalid environment passed to validation.isValidDependentDevicesObject'); console.log(
'debug: Invalid environment passed to validation.isValidDependentDevicesObject',
);
console.log(`\tconfig: ${inspect(e)}`); console.log(`\tconfig: ${inspect(e)}`);
return false; return false;
} }

View File

@ -21,11 +21,11 @@ export class LocalModeManager {
public docker: Docker, public docker: Docker,
public logger: Logger, public logger: Logger,
public db: Database, public db: Database,
) { } ) {}
public async init() { public async init() {
// Setup a listener to catch state changes relating to local mode // Setup a listener to catch state changes relating to local mode
this.config.on('change', (changed) => { this.config.on('change', changed => {
if (changed.localMode != null) { if (changed.localMode != null) {
const localMode = checkTruthy(changed.localMode) || false; const localMode = checkTruthy(changed.localMode) || false;
@ -40,7 +40,9 @@ export class LocalModeManager {
} }
}); });
const localMode = checkTruthy(await this.config.get('localMode') || false); const localMode = checkTruthy(
(await this.config.get('localMode')) || false,
);
if (!localMode) { if (!localMode) {
// Remove any leftovers if necessary // Remove any leftovers if necessary
await this.removeLocalModeArtifacts(); await this.removeLocalModeArtifacts();
@ -52,35 +54,43 @@ export class LocalModeManager {
const images = await this.getLocalModeImages(); const images = await this.getLocalModeImages();
const containers = await this.getLocalModeContainers(images); const containers = await this.getLocalModeContainers(images);
await Bluebird.map(containers, (containerId) => { await Bluebird.map(containers, containerId => {
console.log('Removing local mode container: ', containerId); console.log('Removing local mode container: ', containerId);
return this.docker.getContainer(containerId).remove({ force: true }); return this.docker.getContainer(containerId).remove({ force: true });
}); });
await Bluebird.map(images, (imageId) => { await Bluebird.map(images, imageId => {
console.log('Removing local mode image: ', imageId); console.log('Removing local mode image: ', imageId);
return this.docker.getImage(imageId).remove({ force: true }); return this.docker.getImage(imageId).remove({ force: true });
}); });
// Remove any local mode state added to the database // Remove any local mode state added to the database
await this.db.models('app').del().where({ source: 'local' }); await this.db
.models('app')
.del()
.where({ source: 'local' });
} catch (e) { } catch (e) {
console.log('There was an error clearing local mode artifacts: ', e); console.log('There was an error clearing local mode artifacts: ', e);
} }
} }
private async getLocalModeImages(): Promise<string[]> { private async getLocalModeImages(): Promise<string[]> {
// Return all local mode images present on the local docker daemon // Return all local mode images present on the local docker daemon
return _.map(await this.docker.listImages({ filters: { label: [ 'io.resin.local.image=1' ] } }), 'Id'); return _.map(
await this.docker.listImages({
filters: { label: ['io.resin.local.image=1'] },
}),
'Id',
);
} }
private async getLocalModeContainers(localModeImageIds: string[]): Promise<string[]> { private async getLocalModeContainers(
localModeImageIds: string[],
): Promise<string[]> {
return _(await this.docker.listContainers()) return _(await this.docker.listContainers())
.filter(({ Image }) => _.includes(localModeImageIds, Image)) .filter(({ Image }) => _.includes(localModeImageIds, Image))
.map('Id') .map('Id')
.value(); .value();
} }
} }
export default LocalModeManager; export default LocalModeManager;

View File

@ -46,8 +46,8 @@ export class Logger {
private attached: { private attached: {
[key in OutputStream]: { [containerId: string]: boolean } [key in OutputStream]: { [containerId: string]: boolean }
} = { } = {
[OutputStream.Stderr]: { }, [OutputStream.Stderr]: {},
[OutputStream.Stdout]: { }, [OutputStream.Stdout]: {},
}; };
public constructor({ eventTracker }: LoggerConstructOptions) { public constructor({ eventTracker }: LoggerConstructOptions) {
@ -62,8 +62,7 @@ export class Logger {
offlineMode, offlineMode,
enableLogs, enableLogs,
localMode, localMode,
}: LoggerSetupOptions, }: LoggerSetupOptions) {
) {
this.balenaBackend = new BalenaLogBackend(apiEndpoint, uuid, deviceApiKey); this.balenaBackend = new BalenaLogBackend(apiEndpoint, uuid, deviceApiKey);
this.localBackend = new LocalLogBackend(); this.localBackend = new LocalLogBackend();
@ -126,27 +125,35 @@ export class Logger {
this.log(msgObj); this.log(msgObj);
this.eventTracker.track( this.eventTracker.track(
eventName != null ? eventName : message, eventName != null ? eventName : message,
eventObj != null ? eventObj : { }, eventObj != null ? eventObj : {},
); );
} }
public lock(containerId: string): Bluebird.Disposer<() => void> { public lock(containerId: string): Bluebird.Disposer<() => void> {
return this.writeLock(containerId) return this.writeLock(containerId).disposer(release => {
.disposer((release) => { release();
release(); });
});
} }
public attach( public attach(
docker: Docker, docker: Docker,
containerId: string, containerId: string,
serviceInfo: { serviceId: string, imageId: string }, serviceInfo: { serviceId: string; imageId: string },
): Bluebird<void> { ): Bluebird<void> {
return Bluebird.using(this.lock(containerId), () => { return Bluebird.using(this.lock(containerId), () => {
return this.attachStream(docker, OutputStream.Stdout, containerId, serviceInfo) return this.attachStream(
.then(() => { docker,
return this.attachStream(docker, OutputStream.Stderr, containerId, serviceInfo); OutputStream.Stdout,
}); containerId,
serviceInfo,
).then(() => {
return this.attachStream(
docker,
OutputStream.Stderr,
containerId,
serviceInfo,
);
});
}); });
} }
@ -169,7 +176,7 @@ export class Logger {
public logConfigChange( public logConfigChange(
config: { [configName: string]: string }, config: { [configName: string]: string },
{ success = false, err = null }: { success?: boolean, err?: Error } = { }, { success = false, err = null }: { success?: boolean; err?: Error } = {},
) { ) {
const obj: LogEventObject = { config }; const obj: LogEventObject = { config };
let message: string; let message: string;
@ -177,7 +184,7 @@ export class Logger {
if (success) { if (success) {
message = `Applied configuration change ${JSON.stringify(config)}`; message = `Applied configuration change ${JSON.stringify(config)}`;
eventName = 'Apply config change success'; eventName = 'Apply config change success';
} else if(err != null) { } else if (err != null) {
message = `Error applying configuration change: ${err}`; message = `Error applying configuration change: ${err}`;
eventName = 'Apply config change error'; eventName = 'Apply config change error';
obj.error = err; obj.error = err;
@ -196,9 +203,8 @@ export class Logger {
docker: Docker, docker: Docker,
streamType: OutputStream, streamType: OutputStream,
containerId: string, containerId: string,
{ serviceId, imageId }: { serviceId: string, imageId: string }, { serviceId, imageId }: { serviceId: string; imageId: string },
): Bluebird<void> { ): Bluebird<void> {
return Bluebird.try(() => { return Bluebird.try(() => {
if (this.attached[streamType][containerId]) { if (this.attached[streamType][containerId]) {
return; return;
@ -212,12 +218,14 @@ export class Logger {
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
}; };
return docker.getContainer(containerId).logs(logsOpts) return docker
.then((stream) => { .getContainer(containerId)
.logs(logsOpts)
.then(stream => {
this.attached[streamType][containerId] = true; this.attached[streamType][containerId] = true;
stream stream
.on('error', (err) => { .on('error', err => {
console.error('Error on container logs', err); console.error('Error on container logs', err);
this.attached[streamType][containerId] = false; this.attached[streamType][containerId] = false;
}) })
@ -240,7 +248,7 @@ export class Logger {
this.log(message); this.log(message);
} }
}) })
.on('error', (err) => { .on('error', err => {
console.error('Error on container logs', err); console.error('Error on container logs', err);
this.attached[streamType][containerId] = false; this.attached[streamType][containerId] = false;
}) })
@ -248,16 +256,15 @@ export class Logger {
this.attached[streamType][containerId] = false; this.attached[streamType][containerId] = false;
}); });
}); });
}); });
} }
private objectNameForLogs(eventObj: LogEventObject): string | null { private objectNameForLogs(eventObj: LogEventObject): string | null {
if (eventObj == null) { if (eventObj == null) {
return null; return null;
} }
if (eventObj.service != null && if (
eventObj.service != null &&
eventObj.service.serviceName != null && eventObj.service.serviceName != null &&
eventObj.service.config != null && eventObj.service.config != null &&
eventObj.service.config.image != null eventObj.service.config.image != null
@ -286,20 +293,17 @@ export class Logger {
private static extractContainerMessage( private static extractContainerMessage(
msgBuf: Buffer, msgBuf: Buffer,
): { message: string, timestamp: number } | null { ): { message: string; timestamp: number } | null {
// Non-tty message format from: // Non-tty message format from:
// https://docs.docker.com/engine/api/v1.30/#operation/ContainerAttach // https://docs.docker.com/engine/api/v1.30/#operation/ContainerAttach
if ( if (msgBuf[0] in [0, 1, 2] && _.every(msgBuf.slice(1, 7), c => c === 0)) {
msgBuf[0] in [0, 1, 2] &&
_.every(msgBuf.slice(1, 7), (c) => c === 0)
) {
// Take the header from this message, and parse it as normal // Take the header from this message, and parse it as normal
msgBuf = msgBuf.slice(8); msgBuf = msgBuf.slice(8);
} }
const logLine = msgBuf.toString(); const logLine = msgBuf.toString();
const space = logLine.indexOf(' '); const space = logLine.indexOf(' ');
if (space > 0) { if (space > 0) {
let timestamp = (new Date(logLine.substr(0, space))).getTime(); let timestamp = new Date(logLine.substr(0, space)).getTime();
if (_.isNaN(timestamp)) { if (_.isNaN(timestamp)) {
timestamp = Date.now(); timestamp = Date.now();
} }

View File

@ -21,7 +21,6 @@ interface Options extends url.UrlWithParsedQuery {
} }
export class BalenaLogBackend extends LogBackend { export class BalenaLogBackend extends LogBackend {
private req: ClientRequest | null = null; private req: ClientRequest | null = null;
private dropCount: number = 0; private dropCount: number = 0;
private writable: boolean = true; private writable: boolean = true;
@ -30,11 +29,7 @@ export class BalenaLogBackend extends LogBackend {
private stream: stream.PassThrough; private stream: stream.PassThrough;
timeout: NodeJS.Timer; timeout: NodeJS.Timer;
public constructor( public constructor(apiEndpoint: string, uuid: string, deviceApiKey: string) {
apiEndpoint: string,
uuid: string,
deviceApiKey: string,
) {
super(); super();
this.opts = url.parse(`${apiEndpoint}/device/v2/${uuid}/log-stream`) as any; this.opts = url.parse(`${apiEndpoint}/device/v2/${uuid}/log-stream`) as any;
@ -62,7 +57,9 @@ export class BalenaLogBackend extends LogBackend {
this.flush(); this.flush();
if (this.dropCount > 0) { if (this.dropCount > 0) {
this.write({ this.write({
message: `Warning: Suppressed ${this.dropCount} message(s) due to high load`, message: `Warning: Suppressed ${
this.dropCount
} message(s) due to high load`,
timestamp: Date.now(), timestamp: Date.now(),
isSystem: true, isSystem: true,
isStdErr: true, isStdErr: true,
@ -81,10 +78,13 @@ export class BalenaLogBackend extends LogBackend {
return; return;
} }
message = _.assign({ message = _.assign(
timestamp: Date.now(), {
message: '', timestamp: Date.now(),
}, message); message: '',
},
message,
);
if (!message.isSystem && message.serviceId == null) { if (!message.isSystem && message.serviceId == null) {
return; return;
@ -104,14 +104,17 @@ export class BalenaLogBackend extends LogBackend {
// Since we haven't sent the request body yet, and never will,the // Since we haven't sent the request body yet, and never will,the
// only reason for the server to prematurely respond is to // only reason for the server to prematurely respond is to
// communicate an error. So teardown the connection immediately // communicate an error. So teardown the connection immediately
this.req.on('response', (res) => { this.req.on('response', res => {
console.log('LogBackend: server responded with status code:', res.statusCode); console.log(
'LogBackend: server responded with status code:',
res.statusCode,
);
this.teardown(); this.teardown();
}); });
this.req.on('timeout', () => this.teardown()); this.req.on('timeout', () => this.teardown());
this.req.on('close', () => this.teardown()); this.req.on('close', () => this.teardown());
this.req.on('error', (err) => { this.req.on('error', err => {
console.log('LogBackend: unexpected error:', err); console.log('LogBackend: unexpected error:', err);
this.teardown(); this.teardown();
}); });
@ -120,7 +123,6 @@ export class BalenaLogBackend extends LogBackend {
// respond with potential errors such as 401 authentication error // respond with potential errors such as 401 authentication error
this.req.flushHeaders(); this.req.flushHeaders();
// We want a very low writable high watermark to prevent having many // We want a very low writable high watermark to prevent having many
// chunks stored in the writable queue of @_gzip and have them in // chunks stored in the writable queue of @_gzip and have them in
// @_stream instead. This is desirable because once @_gzip.flush() is // @_stream instead. This is desirable because once @_gzip.flush() is
@ -142,7 +144,6 @@ export class BalenaLogBackend extends LogBackend {
this.flush(); this.flush();
} }
}, RESPONSE_GRACE_PERIOD); }, RESPONSE_GRACE_PERIOD);
}, COOLDOWN_PERIOD); }, COOLDOWN_PERIOD);
private snooze = _.debounce(this.teardown, KEEPALIVE_TIMEOUT); private snooze = _.debounce(this.teardown, KEEPALIVE_TIMEOUT);
@ -150,11 +151,15 @@ export class BalenaLogBackend extends LogBackend {
// Flushing every ZLIB_TIMEOUT hits a balance between compression and // Flushing every ZLIB_TIMEOUT hits a balance between compression and
// latency. When ZLIB_TIMEOUT is 0 the compression ratio is around 5x // latency. When ZLIB_TIMEOUT is 0 the compression ratio is around 5x
// whereas when ZLIB_TIMEOUT is infinity the compession ratio is around 10x. // whereas when ZLIB_TIMEOUT is infinity the compession ratio is around 10x.
private flush = _.throttle(() => { private flush = _.throttle(
if (this.gzip != null) { () => {
this.gzip.flush(zlib.Z_SYNC_FLUSH); if (this.gzip != null) {
} this.gzip.flush(zlib.Z_SYNC_FLUSH);
}, ZLIB_TIMEOUT, { leading: false }); }
},
ZLIB_TIMEOUT,
{ leading: false },
);
private teardown() { private teardown() {
if (this.req != null) { if (this.req != null) {

View File

@ -2,9 +2,4 @@ import { LocalLogBackend } from './local-backend';
import { LogBackend, LogMessage } from './log-backend'; import { LogBackend, LogMessage } from './log-backend';
import { BalenaLogBackend } from './balena-backend'; import { BalenaLogBackend } from './balena-backend';
export { export { LocalLogBackend, LogBackend, LogMessage, BalenaLogBackend };
LocalLogBackend,
LogBackend,
LogMessage,
BalenaLogBackend,
};

View File

@ -6,14 +6,12 @@ import { checkInt } from '../lib/validation';
import { LogBackend, LogMessage } from './log-backend'; import { LogBackend, LogMessage } from './log-backend';
export class LocalLogBackend extends LogBackend { export class LocalLogBackend extends LogBackend {
private globalListeners: Readable[] = []; private globalListeners: Readable[] = [];
private serviceNameResolver: (serviceId: number) => Bluebird<string>; private serviceNameResolver: (serviceId: number) => Bluebird<string>;
public log(message: LogMessage): void { public log(message: LogMessage): void {
if (this.publishEnabled) { if (this.publishEnabled) {
Bluebird.try(() => { Bluebird.try(() => {
if (!message.isSystem) { if (!message.isSystem) {
if (this.serviceNameResolver == null) { if (this.serviceNameResolver == null) {
@ -23,27 +21,29 @@ export class LocalLogBackend extends LogBackend {
} }
const svcId = checkInt(message.serviceId); const svcId = checkInt(message.serviceId);
if (svcId == null) { if (svcId == null) {
console.log('Warning: Non-integer service id found in local logs: '); console.log(
'Warning: Non-integer service id found in local logs: ',
);
console.log(` ${JSON.stringify(message)}`); console.log(` ${JSON.stringify(message)}`);
return null; return null;
} }
// TODO: Can we cache this value? The service ids are reused, so // TODO: Can we cache this value? The service ids are reused, so
// we would need a way of invalidating the cache // we would need a way of invalidating the cache
return this.serviceNameResolver(svcId).then((serviceName) => { return this.serviceNameResolver(svcId).then(serviceName => {
return _.assign({}, { serviceName }, message); return _.assign({}, { serviceName }, message);
}); });
} else { } else {
return message; return message;
} }
}) })
.then((message: LogMessage | null) => { .then((message: LogMessage | null) => {
if (message != null) { if (message != null) {
_.each(this.globalListeners, (listener) => { _.each(this.globalListeners, listener => {
listener.push(`${JSON.stringify(message)}\n`); listener.push(`${JSON.stringify(message)}\n`);
}); });
} }
}) })
.catch((e) => { .catch(e => {
console.log('Error streaming local log output: ', e); console.log('Error streaming local log output: ', e);
}); });
} }
@ -62,10 +62,11 @@ export class LocalLogBackend extends LogBackend {
return stream; return stream;
} }
public assignServiceNameResolver(resolver: (serviceId: number) => Bluebird<string>) { public assignServiceNameResolver(
resolver: (serviceId: number) => Bluebird<string>,
) {
this.serviceNameResolver = resolver; this.serviceNameResolver = resolver;
} }
} }
export default LocalLogBackend; export default LocalLogBackend;

View File

@ -1,4 +1,3 @@
export type LogMessage = Dictionary<any>; export type LogMessage = Dictionary<any>;
export abstract class LogBackend { export abstract class LogBackend {

View File

@ -7,7 +7,7 @@ APIBinder = require './api-binder'
DeviceState = require './device-state' DeviceState = require './device-state'
SupervisorAPI = require './supervisor-api' SupervisorAPI = require './supervisor-api'
{ Logger } = require './logger' { Logger } = require './logger'
{ checkTruthy } = require './lib/validation'; { checkTruthy } = require './lib/validation'
constants = require './lib/constants' constants = require './lib/constants'

View File

@ -7,14 +7,14 @@ _ = require 'lodash'
configs = { configs = {
simple: { simple: {
compose: require('./data/docker-states/simple/compose.json'); compose: require('./data/docker-states/simple/compose.json')
imageInfo: require('./data/docker-states/simple/imageInfo.json'); imageInfo: require('./data/docker-states/simple/imageInfo.json')
inspect: require('./data/docker-states/simple/inspect.json'); inspect: require('./data/docker-states/simple/inspect.json')
} }
entrypoint: { entrypoint: {
compose: require('./data/docker-states/entrypoint/compose.json'); compose: require('./data/docker-states/entrypoint/compose.json')
imageInfo: require('./data/docker-states/entrypoint/imageInfo.json'); imageInfo: require('./data/docker-states/entrypoint/imageInfo.json')
inspect: require('./data/docker-states/entrypoint/inspect.json'); inspect: require('./data/docker-states/entrypoint/inspect.json')
} }
} }
@ -199,8 +199,8 @@ describe 'compose/service', ->
serviceId: 123456, serviceId: 123456,
serviceName: 'test', serviceName: 'test',
ports: [ ports: [
"80:80" '80:80'
"100:100" '100:100'
] ]
}, { appName: 'test' }) }, { appName: 'test' })
@ -302,14 +302,14 @@ describe 'compose/service', ->
}, { appName: 'test' }) }, { appName: 'test' })
expect(makeComposeServiceWithNetwork({ expect(makeComposeServiceWithNetwork({
"balena": { 'balena': {
"ipv4Address": "1.2.3.4" 'ipv4Address': '1.2.3.4'
} }
}).toDockerContainer({ deviceName: 'foo' }).NetworkingConfig).to.deep.equal({ }).toDockerContainer({ deviceName: 'foo' }).NetworkingConfig).to.deep.equal({
EndpointsConfig: { EndpointsConfig: {
"123456_balena": { '123456_balena': {
IPAMConfig: { IPAMConfig: {
IPV4Address: "1.2.3.4" IPV4Address: '1.2.3.4'
}, },
Aliases: [] Aliases: []
} }
@ -325,7 +325,7 @@ describe 'compose/service', ->
} }
}).toDockerContainer({ deviceName: 'foo' }).NetworkingConfig).to.deep.equal({ }).toDockerContainer({ deviceName: 'foo' }).NetworkingConfig).to.deep.equal({
EndpointsConfig: { EndpointsConfig: {
"123456_balena": { '123456_balena': {
IPAMConfig: { IPAMConfig: {
IPV4Address: '1.2.3.4' IPV4Address: '1.2.3.4'
IPV6Address: '5.6.7.8' IPV6Address: '5.6.7.8'
@ -337,23 +337,23 @@ describe 'compose/service', ->
}) })
it 'should correctly convert Docker format to service format', -> it 'should correctly convert Docker format to service format', ->
dockerCfg = require('./data/docker-states/simple/inspect.json'); dockerCfg = require('./data/docker-states/simple/inspect.json')
makeServiceFromDockerWithNetwork = (networks) -> makeServiceFromDockerWithNetwork = (networks) ->
Service.fromDockerContainer( Service.fromDockerContainer(
newConfig = _.cloneDeep(dockerCfg); newConfig = _.cloneDeep(dockerCfg)
newConfig.NetworkSettings = { Networks: networks } newConfig.NetworkSettings = { Networks: networks }
) )
expect(makeServiceFromDockerWithNetwork({ expect(makeServiceFromDockerWithNetwork({
'123456_balena': { '123456_balena': {
IPAMConfig: { IPAMConfig: {
IPv4Address: "1.2.3.4" IPv4Address: '1.2.3.4'
}, },
Aliases: [] Aliases: []
} }
}).config.networks).to.deep.equal({ }).config.networks).to.deep.equal({
'123456_balena': { '123456_balena': {
"ipv4Address": "1.2.3.4" 'ipv4Address': '1.2.3.4'
} }
}) })

View File

@ -225,7 +225,7 @@ describe 'deviceState', ->
@deviceState.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json') @deviceState.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json')
.then => .then =>
@deviceState.getTarget() @deviceState.getTarget()
.then (targetState) => .then (targetState) ->
testTarget = _.cloneDeep(testTarget1) testTarget = _.cloneDeep(testTarget1)
testTarget.local.apps['1234'].services = _.map testTarget.local.apps['1234'].services, (s) -> testTarget.local.apps['1234'].services = _.map testTarget.local.apps['1234'].services, (s) ->
s.imageName = s.image s.imageName = s.image

View File

@ -95,33 +95,33 @@ describe 'EventTracker', ->
it 'should rate limit events of the same type', -> it 'should rate limit events of the same type', ->
@eventTracker.client.track.reset() @eventTracker.client.track.reset()
@eventTracker.track('test', { }); @eventTracker.track('test', {})
@eventTracker.track('test', { }); @eventTracker.track('test', {})
@eventTracker.track('test', { }); @eventTracker.track('test', {})
@eventTracker.track('test', { }); @eventTracker.track('test', {})
@eventTracker.track('test', { }); @eventTracker.track('test', {})
expect(@eventTracker.client.track).to.have.callCount(1) expect(@eventTracker.client.track).to.have.callCount(1)
it 'should rate limit events of the same type with different arguments', -> it 'should rate limit events of the same type with different arguments', ->
@eventTracker.client.track.reset() @eventTracker.client.track.reset()
@eventTracker.track('test2', { a: 1 }); @eventTracker.track('test2', { a: 1 })
@eventTracker.track('test2', { b: 2 }); @eventTracker.track('test2', { b: 2 })
@eventTracker.track('test2', { c: 3 }); @eventTracker.track('test2', { c: 3 })
@eventTracker.track('test2', { d: 4 }); @eventTracker.track('test2', { d: 4 })
@eventTracker.track('test2', { e: 5 }); @eventTracker.track('test2', { e: 5 })
expect(@eventTracker.client.track).to.have.callCount(1) expect(@eventTracker.client.track).to.have.callCount(1)
it 'should not rate limit events of different types', -> it 'should not rate limit events of different types', ->
@eventTracker.client.track.reset() @eventTracker.client.track.reset()
@eventTracker.track('test3', { a: 1 }); @eventTracker.track('test3', { a: 1 })
@eventTracker.track('test4', { b: 2 }); @eventTracker.track('test4', { b: 2 })
@eventTracker.track('test5', { c: 3 }); @eventTracker.track('test5', { c: 3 })
@eventTracker.track('test6', { d: 4 }); @eventTracker.track('test6', { d: 4 })
@eventTracker.track('test7', { e: 5 }); @eventTracker.track('test7', { e: 5 })
expect(@eventTracker.client.track).to.have.callCount(5) expect(@eventTracker.client.track).to.have.callCount(5)

View File

@ -26,7 +26,7 @@ describe 'Logger', ->
track: m.sinon.spy() track: m.sinon.spy()
} }
@logger = new Logger({eventTracker: @fakeEventTracker}) @logger = new Logger({ eventTracker: @fakeEventTracker })
@logger.init({ @logger.init({
apiEndpoint: 'https://example.com' apiEndpoint: 'https://example.com'
uuid: 'deadbeef' uuid: 'deadbeef'
@ -41,7 +41,7 @@ describe 'Logger', ->
it 'waits the grace period before sending any logs', -> it 'waits the grace period before sending any logs', ->
clock = m.sinon.useFakeTimers() clock = m.sinon.useFakeTimers()
@logger.log({message: 'foobar', serviceId: 15}) @logger.log({ message: 'foobar', serviceId: 15 })
clock.tick(4999) clock.tick(4999)
clock.restore() clock.restore()
@ -51,7 +51,7 @@ describe 'Logger', ->
it 'tears down the connection after inactivity', -> it 'tears down the connection after inactivity', ->
clock = m.sinon.useFakeTimers() clock = m.sinon.useFakeTimers()
@logger.log({message: 'foobar', serviceId: 15}) @logger.log({ message: 'foobar', serviceId: 15 })
clock.tick(61000) clock.tick(61000)
clock.restore() clock.restore()

View File

@ -150,20 +150,20 @@ describe 'DeviceConfig', ->
it 'accepts RESIN_ and BALENA_ variables', -> it 'accepts RESIN_ and BALENA_ variables', ->
@deviceConfig.formatConfigKeys({ @deviceConfig.formatConfigKeys({
FOO: 'bar', FOO: 'bar',
BAR: 'baz', BAR: 'baz',
RESIN_HOST_CONFIG_foo: 'foobaz', RESIN_HOST_CONFIG_foo: 'foobaz',
BALENA_HOST_CONFIG_foo: 'foobar', BALENA_HOST_CONFIG_foo: 'foobar',
RESIN_HOST_CONFIG_other: 'val', RESIN_HOST_CONFIG_other: 'val',
BALENA_HOST_CONFIG_baz: 'bad', BALENA_HOST_CONFIG_baz: 'bad',
BALENA_SUPERVISOR_POLL_INTERVAL: '100', BALENA_SUPERVISOR_POLL_INTERVAL: '100',
}).then (filteredConf) -> }).then (filteredConf) ->
expect(filteredConf).to.deep.equal({ expect(filteredConf).to.deep.equal({
HOST_CONFIG_foo: 'foobar', HOST_CONFIG_foo: 'foobar',
HOST_CONFIG_other: 'val', HOST_CONFIG_other: 'val',
HOST_CONFIG_baz: 'bad', HOST_CONFIG_baz: 'bad',
SUPERVISOR_POLL_INTERVAL: '100', SUPERVISOR_POLL_INTERVAL: '100',
}); })
describe 'Extlinux files', -> describe 'Extlinux files', ->

View File

@ -10,13 +10,13 @@ describe 'compose/network', ->
it 'should convert a compose configuration to an internal representation', -> it 'should convert a compose configuration to an internal representation', ->
network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, { network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, {
'driver':'bridge', 'driver': 'bridge',
'ipam':{ 'ipam': {
'driver':'default', 'driver': 'default',
'config':[ 'config': [
{ {
'subnet':'172.25.0.0/25', 'subnet': '172.25.0.0/25',
'gateway':'172.25.0.1' 'gateway': '172.25.0.1'
} }
] ]
} }
@ -30,12 +30,12 @@ describe 'compose/network', ->
subnet: '172.25.0.0/25' subnet: '172.25.0.0/25'
gateway: '172.25.0.1' gateway: '172.25.0.1'
] ]
options: { } options: {}
} }
enableIPv6: false, enableIPv6: false,
internal: false, internal: false,
labels: { } labels: {}
options: { } options: {}
}) })
describe 'internal config -> docker config', -> describe 'internal config -> docker config', ->
@ -43,13 +43,13 @@ describe 'compose/network', ->
it 'should convert an internal representation to a docker representation', -> it 'should convert an internal representation to a docker representation', ->
network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, { network = Network.fromComposeObject({ logger: null, docker: null }, 'test', 123, {
'driver':'bridge', 'driver': 'bridge',
'ipam':{ 'ipam': {
'driver':'default', 'driver': 'default',
'config':[ 'config': [
{ {
'subnet':'172.25.0.0/25', 'subnet': '172.25.0.0/25',
'gateway':'172.25.0.1' 'gateway': '172.25.0.1'
} }
] ]
} }
@ -65,7 +65,7 @@ describe 'compose/network', ->
Subnet: '172.25.0.0/25' Subnet: '172.25.0.0/25'
Gateway: '172.25.0.1' Gateway: '172.25.0.1'
}] }]
Options: { } Options: {}
} }
EnableIPv6: false, EnableIPv6: false,
Internal: false, Internal: false,

View File

@ -1,8 +1,8 @@
require('mocha'); require('mocha')
{ expect } = require('chai'); { expect } = require('chai')
ComposeUtils = require('../src/compose/utils'); ComposeUtils = require('../src/compose/utils')
describe 'Composition utilities', -> describe 'Composition utilities', ->

View File

@ -1,5 +1,4 @@
declare module 'blinking' { declare module 'blinking' {
interface Pattern { interface Pattern {
blinks?: number; blinks?: number;
onDuration?: number; onDuration?: number;
@ -8,7 +7,7 @@ declare module 'blinking' {
} }
interface Blink { interface Blink {
start: (pattern: Pattern) => void start: (pattern: Pattern) => void;
stop: () => void; stop: () => void;
} }

View File

@ -1,7 +1,6 @@
import { ContainerInspectInfo } from 'dockerode'; import { ContainerInspectInfo } from 'dockerode';
declare module 'dockerode' { declare module 'dockerode' {
// Extend the HostConfig interface with the missing fields. // Extend the HostConfig interface with the missing fields.
// TODO: Add these upstream to DefinitelyTyped // TODO: Add these upstream to DefinitelyTyped
interface HostConfig { interface HostConfig {
@ -22,5 +21,4 @@ declare module 'dockerode' {
Healthcheck?: DockerHealthcheck; Healthcheck?: DockerHealthcheck;
StopTimeout?: number; StopTimeout?: number;
} }
} }

View File

@ -1,62 +1,61 @@
// From: https://github.com/icholy/Duration.js/pull/15 // From: https://github.com/icholy/Duration.js/pull/15
// Once the above is merged, use the inbuilt module types // Once the above is merged, use the inbuilt module types
declare module "duration-js" { declare module 'duration-js' {
type DurationLike = Duration | string | number; type DurationLike = Duration | string | number;
type DateLike = Date | number; type DateLike = Date | number;
class Duration { class Duration {
private _milliseconds: number; private _milliseconds: number;
constructor(value?: DurationLike); constructor(value?: DurationLike);
static millisecond: Duration; static millisecond: Duration;
static second: Duration; static second: Duration;
static minute: Duration; static minute: Duration;
static hour: Duration; static hour: Duration;
static day: Duration; static day: Duration;
static week: Duration; static week: Duration;
static milliseconds(milliseconds: number): Duration; static milliseconds(milliseconds: number): Duration;
static seconds(seconds: number): Duration; static seconds(seconds: number): Duration;
static minutes(minutes: number): Duration; static minutes(minutes: number): Duration;
static hours(hours: number): Duration; static hours(hours: number): Duration;
static days(days: number): Duration; static days(days: number): Duration;
static weeks(weeks: number): Duration; static weeks(weeks: number): Duration;
nanoseconds(): number; nanoseconds(): number;
microseconds(): number; microseconds(): number;
milliseconds(): number; milliseconds(): number;
seconds(): number; seconds(): number;
minutes(): number; minutes(): number;
hours(): number; hours(): number;
days(): number; days(): number;
weeks(): number; weeks(): number;
toString(): string; toString(): string;
valueOf(): number; valueOf(): number;
isGreaterThan(duration: DurationLike): boolean; isGreaterThan(duration: DurationLike): boolean;
isLessThan(duration: DurationLike): boolean; isLessThan(duration: DurationLike): boolean;
isEqualTo(duration: DurationLike): boolean; isEqualTo(duration: DurationLike): boolean;
roundTo(duration: DurationLike): void; roundTo(duration: DurationLike): void;
after(date: DateLike): Date; after(date: DateLike): Date;
static since(date: DateLike): Duration; static since(date: DateLike): Duration;
static until(date: DateLike): Duration; static until(date: DateLike): Duration;
static between(a: DateLike, b: DateLike): Duration; static between(a: DateLike, b: DateLike): Duration;
static parse(duration: string): Duration; static parse(duration: string): Duration;
static fromMicroseconds(us: number): Duration; static fromMicroseconds(us: number): Duration;
static fromNanoseconds(ns: number): Duration; static fromNanoseconds(ns: number): Duration;
static add(a: Duration, b: Duration): Duration; static add(a: Duration, b: Duration): Duration;
static subtract(a: Duration, b: Duration): Duration; static subtract(a: Duration, b: Duration): Duration;
static multiply(a: Duration, b: number): Duration; static multiply(a: Duration, b: number): Duration;
static multiply(a: number, b: Duration): Duration; static multiply(a: number, b: Duration): Duration;
static divide(a: Duration, b: Duration): number; static divide(a: Duration, b: Duration): number;
static abs(d: DurationLike): Duration; static abs(d: DurationLike): Duration;
} }
export = Duration; export = Duration;
} }

View File

@ -1,5 +1,4 @@
declare module 'json-mask' { declare module 'json-mask' {
function mask(obj: Dictionary<any>, mask: string): Dictionary<any>; function mask(obj: Dictionary<any>, mask: string): Dictionary<any>;
// These types are not strictly correct, but they don't need to be for our usage // These types are not strictly correct, but they don't need to be for our usage

View File

@ -1,5 +1,5 @@
// Allow importing of json files with typescript // Allow importing of json files with typescript
declare module "*.json" { declare module '*.json' {
const value: { [key: string]: any}; const value: { [key: string]: any };
export default value; export default value;
} }