Merge pull request #1089 from balena-io/convert-tests-to-ts

Convert some coffeescript tests to typescript
This commit is contained in:
CameronDiver 2019-10-30 12:55:13 +00:00 committed by GitHub
commit a57d1df990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 3203 additions and 2633 deletions

83
package-lock.json generated
View File

@ -254,6 +254,15 @@
"integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==",
"dev": true
},
"@types/chai-as-promised": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.2.tgz",
"integrity": "sha512-PO2gcfR3Oxa+u0QvECLe1xKXOqYTzCmWf0FhLhjREoW3fPAVamjihL7v1MOVLJLsnAMdLcjkfrs01yvDMwVK4Q==",
"dev": true,
"requires": {
"@types/chai": "*"
}
},
"@types/common-tags": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.0.tgz",
@ -517,6 +526,16 @@
"integrity": "sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung==",
"dev": true
},
"@types/sinon-chai": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.3.tgz",
"integrity": "sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ==",
"dev": true,
"requires": {
"@types/chai": "*",
"@types/sinon": "*"
}
},
"@types/tmp": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz",
@ -1295,7 +1314,7 @@
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"dev": true,
"requires": {
@ -1323,7 +1342,7 @@
},
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
"dev": true
},
@ -2153,7 +2172,7 @@
},
"cacache": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
"resolved": "http://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz",
"integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==",
"dev": true,
"requires": {
@ -2462,7 +2481,7 @@
"dependencies": {
"coffee-script": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.11.1.tgz",
"resolved": "http://registry.npmjs.org/coffee-script/-/coffee-script-1.11.1.tgz",
"integrity": "sha1-vxxHrWREOg2V0S3ysUfMCk2q1uk=",
"dev": true
},
@ -3123,7 +3142,7 @@
"dependencies": {
"globby": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
"dev": true,
"requires": {
@ -3136,7 +3155,7 @@
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
@ -3481,7 +3500,7 @@
},
"readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"dev": true,
"requires": {
@ -3493,7 +3512,7 @@
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
@ -3926,7 +3945,7 @@
},
"event-stream": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
"integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
"dev": true,
"requires": {
@ -4066,7 +4085,7 @@
"dependencies": {
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
"dev": true
},
@ -6095,13 +6114,13 @@
},
"lodash": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
"dev": true
},
"onetime": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
"dev": true
},
@ -6156,13 +6175,13 @@
"dependencies": {
"bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"resolved": "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=",
"dev": true
},
"lodash": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
"dev": true
}
@ -6838,7 +6857,7 @@
},
"chalk": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz",
"integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==",
"dev": true,
"requires": {
@ -7518,7 +7537,7 @@
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
}
@ -7857,7 +7876,7 @@
},
"map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"resolved": "http://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
"dev": true
},
@ -8142,7 +8161,7 @@
"dependencies": {
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true
},
@ -8250,7 +8269,7 @@
},
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"resolved": "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true
},
@ -8792,7 +8811,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
}
@ -9451,7 +9470,7 @@
},
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
@ -9470,7 +9489,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@ -9652,7 +9671,7 @@
"dependencies": {
"bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"resolved": "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=",
"dev": true
}
@ -9701,7 +9720,7 @@
},
"lodash": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
"dev": true
},
@ -9803,7 +9822,7 @@
},
"resin-register-device": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resin-register-device/-/resin-register-device-3.0.0.tgz",
"resolved": "http://registry.npmjs.org/resin-register-device/-/resin-register-device-3.0.0.tgz",
"integrity": "sha1-PmyWJfOc8jR5K4uQlI8J7AO+JgM=",
"dev": true,
"requires": {
@ -10419,7 +10438,7 @@
},
"source-map": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
"resolved": "http://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
"integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
"dev": true,
"optional": true,
@ -10504,7 +10523,7 @@
},
"split": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
"resolved": "http://registry.npmjs.org/split/-/split-0.3.3.tgz",
"integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
"dev": true,
"requires": {
@ -10740,7 +10759,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
@ -12586,7 +12605,7 @@
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
},
@ -12697,7 +12716,7 @@
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
"integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
"dev": true,
"requires": {
@ -12722,7 +12741,7 @@
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"resolved": "http://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
@ -12733,7 +12752,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"dev": true,
"requires": {

View File

@ -40,6 +40,7 @@
"@balena/contrato": "^0.2.1",
"@types/bluebird": "^3.5.25",
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.2",
"@types/common-tags": "^1.8.0",
"@types/dockerode": "^2.5.20",
"@types/event-stream": "^3.3.34",
@ -57,6 +58,7 @@
"@types/rwlock": "^5.0.2",
"@types/shell-quote": "^1.6.0",
"@types/sinon": "^7.0.13",
"@types/sinon-chai": "^3.2.3",
"@types/tmp": "^0.1.0",
"balena-sync": "^10.0.0",
"blinking": "~0.0.2",

View File

@ -19,6 +19,7 @@ import VolumeManager from './compose/volume-manager';
import Network from './compose/network';
import Service from './compose/service';
import Volume from './compose/volume';
import DockerUtils from './lib/docker-utils';
declare interface Options {
force?: boolean;
@ -45,6 +46,7 @@ export class ApplicationManager extends EventEmitter {
public deviceState: any;
public eventTracker: EventTracker;
public apiBinder: APIBinder;
public docker: DockerUtils;
public services: ServiceManager;
public volumes: VolumeManager;

View File

@ -24,6 +24,11 @@ class DeviceState extends EventEmitter {
public healthcheck(): Promise<void>;
public normaliseLegacy(client: PinejsClientRequest): Promise<void>;
public loadTargetFromFile(filename: string): Promise<void>;
public getTarget(): Promise<any>;
public setTarget(target: any): Promise<any>;
public triggerApplyTarget(opts: any): Promise<any>;
public reportCurrentState(state: any);
public async init();
}

View File

@ -12,8 +12,6 @@ export interface CheckIntOptions {
const ENV_VAR_KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
const LABEL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9\.\-]*$/;
type NullableLiteral = Nullable<number | string>;
/**
* checkInt
*
@ -21,7 +19,7 @@ type NullableLiteral = Nullable<number | string>;
* to be positive
*/
export function checkInt(
s: NullableLiteral,
s: unknown,
options: CheckIntOptions = {},
): number | void {
if (s == null) {
@ -48,7 +46,7 @@ export function checkInt(
*
* 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: unknown): string | void {
if (s == null || !_.isString(s) || _.includes(['null', 'undefined', ''], s)) {
return;
}
@ -62,7 +60,7 @@ export function checkString(s: NullableLiteral): string | void {
* Given a value which can be a string, boolean or number, return a boolean
* which represents if the input was truthy
*/
export function checkTruthy(v: string | boolean | number): boolean | void {
export function checkTruthy(v: unknown): boolean | void {
if (_.isString(v)) {
v = v.toLowerCase();
}
@ -90,7 +88,7 @@ export function checkTruthy(v: string | boolean | number): boolean | void {
* Check that the input string is definitely a string,
* and has a length which is less than 255
*/
export function isValidShortText(t: string): boolean {
export function isValidShortText(t: unknown): boolean {
return _.isString(t) && t.length <= 255;
}

View File

@ -1,17 +0,0 @@
process.env.ROOT_MOUNTPOINT = './test/data'
process.env.BOOT_MOUNTPOINT = '/mnt/boot'
process.env.CONFIG_JSON_PATH = '/config.json'
process.env.DATABASE_PATH = './test/data/database.sqlite'
process.env.DATABASE_PATH_2 = './test/data/database2.sqlite'
process.env.DATABASE_PATH_3 = './test/data/database3.sqlite'
process.env.LED_FILE = './test/data/led_file'
{ stub } = require 'sinon'
dbus = require 'dbus-native'
stub(dbus, 'systemBus').returns({
invoke: (obj, cb) ->
console.log(obj)
cb()
})

18
test/00-init.ts Normal file
View File

@ -0,0 +1,18 @@
process.env.ROOT_MOUNTPOINT = './test/data';
process.env.BOOT_MOUNTPOINT = '/mnt/boot';
process.env.CONFIG_JSON_PATH = '/config.json';
process.env.DATABASE_PATH = './test/data/database.sqlite';
process.env.DATABASE_PATH_2 = './test/data/database2.sqlite';
process.env.DATABASE_PATH_3 = './test/data/database3.sqlite';
process.env.LED_FILE = './test/data/led_file';
import { stub } from 'sinon';
import dbus = require('dbus-native');
stub(dbus, 'systemBus').returns(({
invoke(obj: unknown, cb: () => void) {
console.log(obj);
return cb();
},
} as unknown) as dbus.MessageBus);

View File

@ -1,11 +0,0 @@
prepare = require './lib/prepare'
{ expect } = require './lib/chai-config'
constants = require '../src/lib/constants'
describe 'constants', ->
before ->
prepare()
it 'has the correct configJsonPathOnHost', ->
expect(constants.configJsonPathOnHost).to.equal('/config.json')
it 'has the correct rootMountPoint', ->
expect(constants.rootMountPoint).to.equal('./test/data')

14
test/01-constants.spec.ts Normal file
View File

@ -0,0 +1,14 @@
import ChaiConfig = require('./lib/chai-config');
import prepare = require('./lib/prepare');
const { expect } = ChaiConfig;
import constants = require('../src/lib/constants');
describe('constants', function() {
before(() => prepare());
it('has the correct configJsonPathOnHost', () =>
expect(constants.configJsonPathOnHost).to.equal('/config.json'));
it('has the correct rootMountPoint', () =>
expect(constants.rootMountPoint).to.equal('./test/data'));
});

View File

@ -1,82 +0,0 @@
prepare = require './lib/prepare'
Promise = require 'bluebird'
{ expect } = require './lib/chai-config'
fs = Promise.promisifyAll(require('fs'))
Knex = require('knex')
{ DB } = require('../src/db')
createOldDatabase = (path) ->
knex = new Knex(
client: 'sqlite3'
connection:
filename: path
useNullAsDefault: true
)
createEmptyTable = (name, fn) ->
knex.schema.createTable name, (t) ->
fn(t) if fn?
createEmptyTable 'app', (t) ->
t.increments('id').primary()
t.boolean('privileged')
t.string('containerId')
.then ->
createEmptyTable 'config', (t) ->
t.string('key')
t.string('value')
.then ->
createEmptyTable 'dependentApp', (t) ->
t.increments('id').primary()
.then ->
createEmptyTable 'dependentDevice', (t) ->
t.increments('id').primary()
.then ->
return knex
describe 'DB', ->
before ->
prepare()
@db = new DB()
it 'initializes correctly, running the migrations', ->
expect(@db.init()).to.be.fulfilled
it 'creates a database at the path from an env var', ->
promise = fs.statAsync(process.env.DATABASE_PATH)
expect(promise).to.be.fulfilled
it 'creates a database at the path passed on creation', ->
db2 = new DB({ databasePath: process.env.DATABASE_PATH_2 })
promise = db2.init().then( -> fs.statAsync(process.env.DATABASE_PATH_2))
expect(promise).to.be.fulfilled
it 'adds new fields and removes old ones in an old database', ->
databasePath = process.env.DATABASE_PATH_3
createOldDatabase(databasePath)
.then (knexForDB) ->
db = new DB({ databasePath })
db.init()
.then ->
Promise.all([
expect(knexForDB.schema.hasColumn('app', 'appId')).to.eventually.be.true
expect(knexForDB.schema.hasColumn('app', 'releaseId')).to.eventually.be.true
expect(knexForDB.schema.hasColumn('app', 'config')).to.eventually.be.false
expect(knexForDB.schema.hasColumn('app', 'privileged')).to.eventually.be.false
expect(knexForDB.schema.hasColumn('app', 'containerId')).to.eventually.be.false
expect(knexForDB.schema.hasColumn('dependentApp', 'environment')).to.eventually.be.true
expect(knexForDB.schema.hasColumn('dependentDevice', 'markedForDeletion')).to.eventually.be.true
expect(knexForDB.schema.hasColumn('dependentDevice', 'localId')).to.eventually.be.true
expect(knexForDB.schema.hasColumn('dependentDevice', 'is_managed_by')).to.eventually.be.true
expect(knexForDB.schema.hasColumn('dependentDevice', 'lock_expiry_date')).to.eventually.be.true
])
it 'creates a deviceConfig table with a single default value', ->
promise = @db.models('deviceConfig').select()
Promise.all([
expect(promise).to.eventually.have.lengthOf(1)
expect(promise).to.eventually.deep.equal([ { targetValues: '{}' } ])
])
it 'allows performing transactions', ->
@db.transaction (trx) ->
expect(trx.commit()).to.be.fulfilled

108
test/02-db.spec.ts Normal file
View File

@ -0,0 +1,108 @@
import * as Bluebird from 'bluebird';
import * as Knex from 'knex';
import { fs } from 'mz';
import ChaiConfig = require('./lib/chai-config');
import prepare = require('./lib/prepare');
import { DB } from '../src/db';
const { expect } = ChaiConfig;
async function createOldDatabase(path: string) {
const knex = Knex({
client: 'sqlite3',
connection: {
filename: path,
},
useNullAsDefault: true,
});
const createEmptyTable = (
name: string,
fn: (trx: Knex.CreateTableBuilder) => void,
) =>
knex.schema.createTable(name, t => {
if (fn != null) {
return fn(t);
}
});
await createEmptyTable('app', t => {
t.increments('id').primary();
t.boolean('privileged');
return t.string('containerId');
});
await createEmptyTable('config', t => {
t.string('key');
return t.string('value');
});
await createEmptyTable('dependentApp', t => t.increments('id').primary());
await createEmptyTable('dependentDevice', t => t.increments('id').primary());
return knex;
}
describe('DB', () => {
let db: DB;
before(() => {
prepare();
db = new DB();
});
it('initializes correctly, running the migrations', () => {
return expect(db.init()).to.be.fulfilled;
});
it('creates a database at the path from an env var', () => {
const promise = fs.stat(process.env.DATABASE_PATH!);
return expect(promise).to.be.fulfilled;
});
it('creates a database at the path passed on creation', () => {
const db2 = new DB({ databasePath: process.env.DATABASE_PATH_2 });
const promise = db2
.init()
.then(() => fs.stat(process.env.DATABASE_PATH_2!));
return expect(promise).to.be.fulfilled;
});
it('adds new fields and removes old ones in an old database', async () => {
const databasePath = process.env.DATABASE_PATH_3!;
const knexForDB = await createOldDatabase(databasePath);
const testDb = new DB({ databasePath });
await testDb.init();
await Bluebird.all([
expect(knexForDB.schema.hasColumn('app', 'appId')).to.eventually.be.true,
expect(knexForDB.schema.hasColumn('app', 'releaseId')).to.eventually.be
.true,
expect(knexForDB.schema.hasColumn('app', 'config')).to.eventually.be
.false,
expect(knexForDB.schema.hasColumn('app', 'privileged')).to.eventually.be
.false,
expect(knexForDB.schema.hasColumn('app', 'containerId')).to.eventually.be
.false,
expect(knexForDB.schema.hasColumn('dependentApp', 'environment')).to
.eventually.be.true,
expect(knexForDB.schema.hasColumn('dependentDevice', 'markedForDeletion'))
.to.eventually.be.true,
expect(knexForDB.schema.hasColumn('dependentDevice', 'localId')).to
.eventually.be.true,
expect(knexForDB.schema.hasColumn('dependentDevice', 'is_managed_by')).to
.eventually.be.true,
expect(knexForDB.schema.hasColumn('dependentDevice', 'lock_expiry_date'))
.to.eventually.be.true,
]);
});
it('creates a deviceConfig table with a single default value', async () => {
const deviceConfig = await db.models('deviceConfig').select();
expect(deviceConfig).to.have.lengthOf(1);
expect(deviceConfig).to.deep.equal([{ targetValues: '{}' }]);
});
it('allows performing transactions', () => {
return db.transaction(trx => expect(trx.commit()).to.be.fulfilled);
});
});

View File

@ -1,126 +0,0 @@
prepare = require './lib/prepare'
Promise = require 'bluebird'
chai = require './lib/chai-config'
chai.use(require('chai-events'))
{ expect } = chai
fs = Promise.promisifyAll(require('fs'))
{ DB } = require('../src/db')
{ Config } = require('../src/config')
constants = require('../src/lib/constants')
describe 'Config', ->
before ->
prepare()
@db = new DB()
@conf = new Config({ @db })
@initialization = @db.init().then =>
@conf.init()
it 'uses the correct config.json path', ->
expect(@conf.configJsonBackend.path()).to.eventually.equal('test/data/config.json')
it 'uses the correct config.json path from the root mount when passed as argument to the constructor', ->
conf2 = new Config({ @db, configPath: '/foo.json' })
expect(conf2.configJsonBackend.path()).to.eventually.equal('test/data/foo.json')
it 'initializes correctly', ->
expect(@initialization).to.be.fulfilled
it 'reads and exposes values from the config.json', ->
promise = @conf.get('applicationId')
expect(promise).to.eventually.equal(78373)
it 'allows reading several values in one getMany call', ->
promise = @conf.getMany([ 'applicationId', 'apiEndpoint' ])
expect(promise).to.eventually.deep.equal({ applicationId: 78373, apiEndpoint: 'https://api.resin.io' })
it 'generates a uuid and stores it in config.json', ->
promise = @conf.get('uuid')
promise2 = fs.readFileAsync('./test/data/config.json').then(JSON.parse).get('uuid')
Promise.all([
expect(promise).to.be.fulfilled
expect(promise2).to.be.fulfilled
]).then ([uuid1, uuid2]) ->
expect(uuid1).to.be.a('string')
expect(uuid1).to.have.lengthOf(62)
expect(uuid1).to.equal(uuid2)
it 'does not allow setting an immutable field', ->
promise = @conf.set({ username: 'somebody else' })
# We catch it to avoid the unhandled error log
promise.catch(->)
expect(promise).to.be.rejected
it 'allows setting both config.json and database fields transparently', ->
promise = @conf.set({ appUpdatePollInterval: 30000, name: 'a new device name' }).then =>
@conf.getMany([ 'appUpdatePollInterval', 'name' ])
expect(promise).to.eventually.deep.equal({ appUpdatePollInterval: 30000, name: 'a new device name' })
it 'allows removing a db key', ->
promise = @conf.remove('apiSecret').then =>
@conf.get('apiSecret')
expect(promise).to.be.fulfilled
expect(promise).to.eventually.be.undefined
it 'allows deleting a config.json key and returns a default value if none is set', ->
promise = @conf.remove('appUpdatePollInterval').then =>
@conf.get('appUpdatePollInterval')
expect(promise).to.be.fulfilled
expect(promise).to.eventually.equal(60000)
it 'allows deleting a config.json key if it is null', ->
promise = @conf.set('apiKey': null).then =>
@conf.get('apiKey')
expect(promise).to.be.fulfilled
expect(promise).to.eventually.be.undefined
.then ->
fs.readFileAsync('./test/data/config.json')
.then(JSON.parse)
.then (confFromFile) ->
expect(confFromFile).to.not.have.property('apiKey')
it 'does not allow modifying or removing a function value', ->
promise1 = @conf.remove('version')
promise1.catch(->)
promise2 = @conf.set(version: '2.0')
promise2.catch(->)
Promise.all([
expect(promise1).to.be.rejected
expect(promise2).to.be.rejected
])
it 'throws when asked for an unknown key', ->
promise = @conf.get('unknownInvalidValue')
promise.catch(->)
expect(promise).to.be.rejected
it 'emits a change event when values are set', (done) ->
@conf.on 'change', (val) ->
expect(val).to.deep.equal({ name: 'someValue' })
done()
@conf.set({ name: 'someValue' })
expect(@conf).to.emit('change')
return
it "returns an undefined OS variant if it doesn't exist", ->
oldPath = constants.hostOSVersionPath
constants.hostOSVersionPath = 'test/data/etc/os-release-novariant'
@conf.get('osVariant')
.then (osVariant) ->
constants.hostOSVersionPath = oldPath
expect(osVariant).to.be.undefined
describe 'Function config providers', ->
before ->
prepare()
@db = new DB()
@conf = new Config({ @db })
@initialization = @db.init().then =>
@conf.init()
it 'should throw if a non-mutable function provider is set', ->
expect(@conf.set({ version: 'some-version' })).to.be.rejected
it 'should throw if a non-mutbale function provider is removed', ->
expect(@conf.remove('version')).to.be.rejected

154
test/03-config.spec.ts Normal file
View File

@ -0,0 +1,154 @@
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import { fs } from 'mz';
import chai = require('./lib/chai-config');
import prepare = require('./lib/prepare');
// tslint:disable-next-line
chai.use(require('chai-events'));
const { expect } = chai;
import Config from '../src/config';
import DB from '../src/db';
import constants = require('../src/lib/constants');
describe('Config', () => {
let db: DB;
let conf: Config;
let initialization: Bluebird<unknown>;
before(() => {
prepare();
db = new DB();
conf = new Config({ db });
initialization = db.init().then(() => conf.init());
});
it('uses the correct config.json path', async () => {
expect(await (conf as any).configJsonBackend.path()).to.equal(
'test/data/config.json',
);
});
it('uses the correct config.json path from the root mount when passed as argument to the constructor', async () => {
const conf2 = new Config({ db, configPath: '/foo.json' });
expect(await (conf2 as any).configJsonBackend.path()).to.equal(
'test/data/foo.json',
);
});
it('initializes correctly', () => {
return expect(initialization).to.be.fulfilled;
});
it('reads and exposes values from the config.json', async () => {
const id = await conf.get('applicationId');
return expect(id).to.equal(78373);
});
it('allows reading several values in one getMany call', async () => {
return expect(
await conf.getMany(['applicationId', 'apiEndpoint']),
).to.deep.equal({
applicationId: 78373,
apiEndpoint: 'https://api.resin.io',
});
});
it('generates a uuid and stores it in config.json', async () => {
const uuid = await conf.get('uuid');
const configJsonUuid = JSON.parse(
await fs.readFile('./test/data/config.json', 'utf8'),
).uuid;
expect(uuid).to.be.a('string');
expect(uuid).to.have.lengthOf(62);
expect(uuid).to.equal(configJsonUuid);
});
it('does not allow setting an immutable field', async () => {
const promise = conf.set({ username: 'somebody else' });
// We catch it to avoid the unhandled error log
promise.catch(_.noop);
return expect(promise).to.be.rejected;
});
it('allows setting both config.json and database fields transparently', async () => {
await conf.set({ appUpdatePollInterval: 30000, name: 'a new device name' });
const config = await conf.getMany(['appUpdatePollInterval', 'name']);
return expect(config).to.deep.equal({
appUpdatePollInterval: 30000,
name: 'a new device name',
});
});
it('allows removing a db key', async () => {
await conf.remove('apiSecret');
const secret = await conf.get('apiSecret');
return expect(secret).to.be.undefined;
});
it('allows deleting a config.json key and returns a default value if none is set', async () => {
await conf.remove('appUpdatePollInterval');
const poll = await conf.get('appUpdatePollInterval');
return expect(poll).to.equal(60000);
});
it('allows deleting a config.json key if it is null', async () => {
await conf.set({ apiKey: null });
const key = await conf.get('apiKey');
expect(key).to.be.undefined;
expect(
JSON.parse(await fs.readFile('./test/data/config.json', 'utf8')),
).to.not.have.property('apiKey');
});
it('does not allow modifying or removing a function value', () => {
// We have to cast to any below, as the type system will
// not allow removing a function value
expect(conf.remove('version' as any)).to.be.rejected;
expect(conf.set({ version: '2.0' })).to.be.rejected;
});
it('throws when asked for an unknown key', () => {
expect(conf.get('unknownInvalidValue' as any)).to.be.rejected;
});
it('emits a change event when values are set', done => {
conf.on('change', val => {
expect(val).to.deep.equal({ name: 'someValue' });
return done();
});
conf.set({ name: 'someValue' });
(expect(conf).to as any).emit('change');
});
it("returns an undefined OS variant if it doesn't exist", async () => {
const oldPath = constants.hostOSVersionPath;
constants.hostOSVersionPath = 'test/data/etc/os-release-novariant';
const osVariant = await conf.get('osVariant');
constants.hostOSVersionPath = oldPath;
expect(osVariant).to.be.undefined;
});
describe('Function config providers', () => {
before(async () => {
prepare();
db = new DB();
conf = new Config({ db });
await db.init();
await conf.init();
});
it('should throw if a non-mutable function provider is set', () => {
expect(conf.set({ version: 'some-version' })).to.be.rejected;
});
it('should throw if a non-mutable function provider is removed', () => {
expect(conf.remove('version' as any)).to.be.rejected;
});
});
});

View File

@ -1,548 +0,0 @@
{ assert, expect } = require './lib/chai-config'
_ = require 'lodash'
{ Service } = require '../src/compose/service'
configs = {
simple: {
compose: require('./data/docker-states/simple/compose.json')
imageInfo: require('./data/docker-states/simple/imageInfo.json')
inspect: require('./data/docker-states/simple/inspect.json')
}
entrypoint: {
compose: require('./data/docker-states/entrypoint/compose.json')
imageInfo: require('./data/docker-states/entrypoint/imageInfo.json')
inspect: require('./data/docker-states/entrypoint/inspect.json')
},
networkModeService: {
compose: require('./data/docker-states/network-mode-service/compose.json')
imageInfo: require('./data/docker-states/network-mode-service/imageInfo.json')
inspect: require('./data/docker-states/network-mode-service/inspect.json')
}
}
describe 'compose/service', ->
it 'extends environment variables properly', ->
extendEnvVarsOpts = {
uuid: '1234'
appName: 'awesomeApp'
commit: 'abcdef'
name: 'awesomeDevice'
version: 'v1.0.0'
deviceType: 'raspberry-pi'
osVersion: 'Resin OS 2.0.2'
}
service = {
appId: '23'
releaseId: 2
serviceId: 3
imageId: 4
serviceName: 'serviceName'
environment:
FOO: 'bar'
A_VARIABLE: 'ITS_VALUE'
}
s = Service.fromComposeObject(service, extendEnvVarsOpts)
expect(s.config.environment).to.deep.equal({
FOO: 'bar'
A_VARIABLE: 'ITS_VALUE'
RESIN_APP_ID: '23'
RESIN_APP_NAME: 'awesomeApp'
RESIN_DEVICE_UUID: '1234'
RESIN_DEVICE_TYPE: 'raspberry-pi'
RESIN_HOST_OS_VERSION: 'Resin OS 2.0.2'
RESIN_SERVICE_NAME: 'serviceName'
RESIN_SUPERVISOR_VERSION: 'v1.0.0'
RESIN_APP_LOCK_PATH: '/tmp/balena/updates.lock'
RESIN_SERVICE_KILL_ME_PATH: '/tmp/balena/handover-complete'
RESIN: '1'
BALENA_APP_ID: '23'
BALENA_APP_NAME: 'awesomeApp'
BALENA_DEVICE_UUID: '1234'
BALENA_DEVICE_TYPE: 'raspberry-pi'
BALENA_HOST_OS_VERSION: 'Resin OS 2.0.2'
BALENA_SERVICE_NAME: 'serviceName'
BALENA_SUPERVISOR_VERSION: 'v1.0.0'
BALENA_APP_LOCK_PATH: '/tmp/balena/updates.lock'
BALENA_SERVICE_HANDOVER_COMPLETE_PATH: '/tmp/balena/handover-complete'
BALENA: '1'
USER: 'root'
})
it 'returns the correct default bind mounts', ->
s = Service.fromComposeObject({
appId: '1234'
serviceName: 'foo'
releaseId: 2
serviceId: 3
imageId: 4
}, { appName: 'foo' })
binds = Service.defaultBinds(s.appId, s.serviceName)
expect(binds).to.deep.equal([
'/tmp/balena-supervisor/services/1234/foo:/tmp/resin'
'/tmp/balena-supervisor/services/1234/foo:/tmp/balena'
])
it 'produces the correct port bindings and exposed ports', ->
s = Service.fromComposeObject({
appId: '1234'
serviceName: 'foo'
releaseId: 2
serviceId: 3
imageId: 4
expose: [
1000,
'243/udp'
],
ports: [
'2344'
'2345:2354'
'2346:2367/udp'
]
}, {
imageInfo: Config: {
ExposedPorts: {
'53/tcp': {}
'53/udp': {}
'2354/tcp': {}
}
}
})
ports = s.generateExposeAndPorts()
expect(ports.portBindings).to.deep.equal({
'2344/tcp': [{
HostIp: '',
HostPort: '2344'
}],
'2354/tcp': [{
HostIp: '',
HostPort: '2345'
}],
'2367/udp': [{
HostIp: '',
HostPort: '2346'
}]
})
expect(ports.exposedPorts).to.deep.equal({
'1000/tcp': {}
'243/udp': {}
'2344/tcp': {}
'2354/tcp': {}
'2367/udp': {}
'53/tcp': {}
'53/udp': {}
})
it 'correctly handles port ranges', ->
s = Service.fromComposeObject({
appId: '1234'
serviceName: 'foo'
releaseId: 2
serviceId: 3
imageId: 4
expose: [
1000,
'243/udp'
],
ports: [
'1000-1003:2000-2003'
]
}, { appName: 'test' })
ports = s.generateExposeAndPorts()
expect(ports.portBindings).to.deep.equal({
'2000/tcp': [
HostIp: ''
HostPort: '1000'
],
'2001/tcp': [
HostIp: ''
HostPort: '1001'
],
'2002/tcp': [
HostIp: ''
HostPort: '1002'
],
'2003/tcp': [
HostIp: ''
HostPort: '1003'
]
})
expect(ports.exposedPorts).to.deep.equal({
'1000/tcp': {}
'2000/tcp': {}
'2001/tcp': {}
'2002/tcp': {}
'2003/tcp': {}
'243/udp': {}
})
it 'should correctly handle large port ranges', ->
@timeout(60000)
s = Service.fromComposeObject({
appId: '1234'
serviceName: 'foo'
releaseId: 2
serviceId: 3
imageId: 4
ports: [
'5-65536:5-65536/tcp'
'5-65536:5-65536/udp'
]
}, { appName: 'test' })
expect(s.generateExposeAndPorts()).to.not.throw
it 'should correctly report implied exposed ports from portMappings', ->
service = Service.fromComposeObject({
appId: 123456,
serviceId: 123456,
serviceName: 'test',
ports: [
'80:80'
'100:100'
]
}, { appName: 'test' })
expect(service.config).to.have.property('expose').that.deep.equals(['80/tcp', '100/tcp'])
describe 'Ordered array parameters', ->
it 'Should correctly compare ordered array parameters', ->
svc1 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
dns: [
'8.8.8.8',
'1.1.1.1',
]
}, { appName: 'test' })
svc2 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
dns: [
'8.8.8.8',
'1.1.1.1',
]
}, { appName: 'test' })
assert(svc1.isEqualConfig(svc2))
svc2 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
dns: [
'1.1.1.1',
'8.8.8.8',
]
}, { appName: 'test' })
assert(!svc1.isEqualConfig(svc2))
it 'should correctly compare non-ordered array parameters', ->
svc1 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: [
'abcdef',
'ghijk',
]
}, { appName: 'test' })
svc2 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: [
'abcdef',
'ghijk',
]
}, { appName: 'test' })
assert(svc1.isEqualConfig(svc2))
svc2 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: [
'ghijk',
'abcdef',
]
}, { appName: 'test' })
assert(svc1.isEqualConfig(svc2))
it 'should correctly compare both ordered and non-ordered array parameters', ->
svc1 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: [
'abcdef',
'ghijk',
],
dns: [
'8.8.8.8',
'1.1.1.1',
]
}, { appName: 'test' })
svc2 = Service.fromComposeObject({
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: [
'ghijk',
'abcdef',
],
dns: [
'8.8.8.8',
'1.1.1.1',
]
}, { appName: 'test' })
assert(svc1.isEqualConfig(svc2))
describe 'parseMemoryNumber()', ->
makeComposeServiceWithLimit = (memLimit) ->
Service.fromComposeObject({
appId: 123456
serviceId: 123456
serviceName: 'foobar'
mem_limit: memLimit
}, { appName: 'test' })
it 'should correctly parse memory number strings without a unit', ->
expect(makeComposeServiceWithLimit('64').config.memLimit).to.equal(64)
it 'should correctly apply the default value', ->
expect(makeComposeServiceWithLimit(undefined).config.memLimit).to.equal(0)
it 'should correctly support parsing numbers as memory limits', ->
expect(makeComposeServiceWithLimit(64).config.memLimit).to.equal(64)
it 'should correctly parse memory number strings that use a byte unit', ->
expect(makeComposeServiceWithLimit('64b').config.memLimit).to.equal(64)
expect(makeComposeServiceWithLimit('64B').config.memLimit).to.equal(64)
it 'should correctly parse memory number strings that use a kilobyte unit', ->
expect(makeComposeServiceWithLimit('64k').config.memLimit).to.equal(65536)
expect(makeComposeServiceWithLimit('64K').config.memLimit).to.equal(65536)
expect(makeComposeServiceWithLimit('64kb').config.memLimit).to.equal(65536)
expect(makeComposeServiceWithLimit('64Kb').config.memLimit).to.equal(65536)
it 'should correctly parse memory number strings that use a megabyte unit', ->
expect(makeComposeServiceWithLimit('64m').config.memLimit).to.equal(67108864)
expect(makeComposeServiceWithLimit('64M').config.memLimit).to.equal(67108864)
expect(makeComposeServiceWithLimit('64mb').config.memLimit).to.equal(67108864)
expect(makeComposeServiceWithLimit('64Mb').config.memLimit).to.equal(67108864)
it 'should correctly parse memory number strings that use a gigabyte unit', ->
expect(makeComposeServiceWithLimit('64g').config.memLimit).to.equal(68719476736)
expect(makeComposeServiceWithLimit('64G').config.memLimit).to.equal(68719476736)
expect(makeComposeServiceWithLimit('64gb').config.memLimit).to.equal(68719476736)
expect(makeComposeServiceWithLimit('64Gb').config.memLimit).to.equal(68719476736)
describe 'getWorkingDir', ->
makeComposeServiceWithWorkdir = (workdir) ->
Service.fromComposeObject({
appId: 123456,
serviceId: 123456,
serviceName: 'foobar'
workingDir: workdir
}, { appName: 'test' })
it 'should remove a trailing slash', ->
expect(makeComposeServiceWithWorkdir('/usr/src/app/').config.workingDir).to.equal('/usr/src/app')
expect(makeComposeServiceWithWorkdir('/').config.workingDir).to.equal('/')
expect(makeComposeServiceWithWorkdir('/usr/src/app').config.workingDir).to.equal('/usr/src/app')
expect(makeComposeServiceWithWorkdir('').config.workingDir).to.equal('')
describe 'Docker <-> Compose config', ->
omitConfigForComparison = (config) ->
return _.omit(config, ['running', 'networks'])
it 'should be identical when converting a simple service', ->
composeSvc = Service.fromComposeObject(configs.simple.compose, configs.simple.imageInfo)
dockerSvc = Service.fromDockerContainer(configs.simple.inspect)
composeConfig = omitConfigForComparison(composeSvc.config)
dockerConfig = omitConfigForComparison(dockerSvc.config)
expect(composeConfig).to.deep.equal(dockerConfig)
expect(dockerSvc.isEqualConfig(composeSvc)).to.be.true
it 'should correct convert formats with a null entrypoint', ->
composeSvc = Service.fromComposeObject(configs.entrypoint.compose, configs.entrypoint.imageInfo)
dockerSvc = Service.fromDockerContainer(configs.entrypoint.inspect)
composeConfig = omitConfigForComparison(composeSvc.config)
dockerConfig = omitConfigForComparison(dockerSvc.config)
expect(composeConfig).to.deep.equal(dockerConfig)
expect(dockerSvc.isEqualConfig(composeSvc)).to.equals(true)
describe 'Networks', ->
it 'should correctly convert networks from compose to docker format', ->
makeComposeServiceWithNetwork = (networks) ->
Service.fromComposeObject({
appId: 123456,
serviceId: 123456,
serviceName: 'test',
networks
}, { appName: 'test' })
expect(makeComposeServiceWithNetwork({
'balena': {
'ipv4Address': '1.2.3.4'
}
}).toDockerContainer({ deviceName: 'foo' }).NetworkingConfig).to.deep.equal({
EndpointsConfig: {
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4'
},
Aliases: [
'test'
]
}
}
})
expect(makeComposeServiceWithNetwork({
balena: {
aliases: [ 'test', '1123']
ipv4Address: '1.2.3.4'
ipv6Address: '5.6.7.8'
linkLocalIps: [ '123.123.123' ]
}
}).toDockerContainer({ deviceName: 'foo' }).NetworkingConfig).to.deep.equal({
EndpointsConfig: {
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4'
IPv6Address: '5.6.7.8'
LinkLocalIPs: [ '123.123.123' ]
}
Aliases: [ 'test', '1123' ]
}
}
})
it 'should correctly convert Docker format to service format', ->
dockerCfg = require('./data/docker-states/simple/inspect.json')
makeServiceFromDockerWithNetwork = (networks) ->
Service.fromDockerContainer(
newConfig = _.cloneDeep(dockerCfg)
newConfig.NetworkSettings = { Networks: networks }
)
expect(makeServiceFromDockerWithNetwork({
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4'
},
Aliases: []
}
}).config.networks).to.deep.equal({
'123456_balena': {
'ipv4Address': '1.2.3.4'
}
})
expect(makeServiceFromDockerWithNetwork({
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4'
IPv6Address: '5.6.7.8'
LinkLocalIps: [ '123.123.123' ]
}
Aliases: [ 'test', '1123' ]
}
}).config.networks).to.deep.equal({
'123456_balena': {
ipv4Address: '1.2.3.4'
ipv6Address: '5.6.7.8'
linkLocalIps: [ '123.123.123' ]
aliases: [ 'test', '1123' ]
}
})
describe 'Network mode=service:', ->
it 'should correctly add a depends_on entry for the service', ->
s = Service.fromComposeObject({
appId: '1234'
serviceName: 'foo'
releaseId: 2
serviceId: 3
imageId: 4
network_mode: 'service: test'
}, { appName: 'test' })
expect(s.dependsOn).to.deep.equal(['test'])
s = Service.fromComposeObject({
appId: '1234'
serviceName: 'foo'
releaseId: 2
serviceId: 3
imageId: 4
depends_on: [
'another_service'
]
network_mode: 'service: test'
}, { appName: 'test' })
expect(s.dependsOn).to.deep.equal([
'another_service',
'test'
])
it 'should correctly convert a network_mode service: to a container:', ->
s = Service.fromComposeObject({
appId: '1234'
serviceName: 'foo'
releaseId: 2
serviceId: 3
imageId: 4
network_mode: 'service: test'
}, { appName: 'test' })
expect(s.toDockerContainer({ deviceName: '', containerIds: { test: 'abcdef' } }))
.to.have.property('HostConfig')
.that.has.property('NetworkMode')
.that.equals('container:abcdef')
it 'should not cause a container restart if a service: container has not changed', ->
composeSvc = Service.fromComposeObject(configs.networkModeService.compose, configs.networkModeService.imageInfo)
dockerSvc = Service.fromDockerContainer(configs.networkModeService.inspect)
composeConfig = omitConfigForComparison(composeSvc.config)
dockerConfig = omitConfigForComparison(dockerSvc.config)
expect(composeConfig).to.not.deep.equal(dockerConfig)
expect(dockerSvc.isEqualConfig(
composeSvc,
{ test: 'abcdef' }
)).to.be.true
it 'should restart a container when its dependent network mode container changes', ->
composeSvc = Service.fromComposeObject(configs.networkModeService.compose, configs.networkModeService.imageInfo)
dockerSvc = Service.fromDockerContainer(configs.networkModeService.inspect)
composeConfig = omitConfigForComparison(composeSvc.config)
dockerConfig = omitConfigForComparison(dockerSvc.config)
expect(composeConfig).to.not.deep.equal(dockerConfig)
expect(dockerSvc.isEqualConfig(
composeSvc,
{ test: 'qwerty' }
)).to.be.false

662
test/04-service.spec.ts Normal file
View File

@ -0,0 +1,662 @@
import * as _ from 'lodash';
import ChaiConfig = require('./lib/chai-config');
const { assert, expect } = ChaiConfig;
import Service from '../src/compose/service';
import {
ServiceComposeConfig,
ServiceConfig,
} from '../src/compose/types/service';
const configs = {
simple: {
compose: require('./data/docker-states/simple/compose.json'),
imageInfo: require('./data/docker-states/simple/imageInfo.json'),
inspect: require('./data/docker-states/simple/inspect.json'),
},
entrypoint: {
compose: require('./data/docker-states/entrypoint/compose.json'),
imageInfo: require('./data/docker-states/entrypoint/imageInfo.json'),
inspect: require('./data/docker-states/entrypoint/inspect.json'),
},
networkModeService: {
compose: require('./data/docker-states/network-mode-service/compose.json'),
imageInfo: require('./data/docker-states/network-mode-service/imageInfo.json'),
inspect: require('./data/docker-states/network-mode-service/inspect.json'),
},
};
describe('compose/service', () => {
it('extends environment variables properly', () => {
const extendEnvVarsOpts = {
uuid: '1234',
appName: 'awesomeApp',
commit: 'abcdef',
name: 'awesomeDevice',
version: 'v1.0.0',
deviceType: 'raspberry-pi',
osVersion: 'Resin OS 2.0.2',
};
const service = {
appId: '23',
releaseId: 2,
serviceId: 3,
imageId: 4,
serviceName: 'serviceName',
environment: {
FOO: 'bar',
A_VARIABLE: 'ITS_VALUE',
},
};
const s = Service.fromComposeObject(service, extendEnvVarsOpts as any);
expect(s.config.environment).to.deep.equal({
FOO: 'bar',
A_VARIABLE: 'ITS_VALUE',
RESIN_APP_ID: '23',
RESIN_APP_NAME: 'awesomeApp',
RESIN_DEVICE_UUID: '1234',
RESIN_DEVICE_TYPE: 'raspberry-pi',
RESIN_HOST_OS_VERSION: 'Resin OS 2.0.2',
RESIN_SERVICE_NAME: 'serviceName',
RESIN_SUPERVISOR_VERSION: 'v1.0.0',
RESIN_APP_LOCK_PATH: '/tmp/balena/updates.lock',
RESIN_SERVICE_KILL_ME_PATH: '/tmp/balena/handover-complete',
RESIN: '1',
BALENA_APP_ID: '23',
BALENA_APP_NAME: 'awesomeApp',
BALENA_DEVICE_UUID: '1234',
BALENA_DEVICE_TYPE: 'raspberry-pi',
BALENA_HOST_OS_VERSION: 'Resin OS 2.0.2',
BALENA_SERVICE_NAME: 'serviceName',
BALENA_SUPERVISOR_VERSION: 'v1.0.0',
BALENA_APP_LOCK_PATH: '/tmp/balena/updates.lock',
BALENA_SERVICE_HANDOVER_COMPLETE_PATH: '/tmp/balena/handover-complete',
BALENA: '1',
USER: 'root',
});
});
it('returns the correct default bind mounts', () => {
const s = Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
releaseId: 2,
serviceId: 3,
imageId: 4,
},
{ appName: 'foo' } as any,
);
const binds = (Service as any).defaultBinds(s.appId, s.serviceName);
expect(binds).to.deep.equal([
'/tmp/balena-supervisor/services/1234/foo:/tmp/resin',
'/tmp/balena-supervisor/services/1234/foo:/tmp/balena',
]);
});
it('produces the correct port bindings and exposed ports', () => {
const s = Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
releaseId: 2,
serviceId: 3,
imageId: 4,
expose: [1000, '243/udp'],
ports: ['2344', '2345:2354', '2346:2367/udp'],
},
{
imageInfo: {
Config: {
ExposedPorts: {
'53/tcp': {},
'53/udp': {},
'2354/tcp': {},
},
},
},
} as any,
);
const ports = (s as any).generateExposeAndPorts();
expect(ports.portBindings).to.deep.equal({
'2344/tcp': [
{
HostIp: '',
HostPort: '2344',
},
],
'2354/tcp': [
{
HostIp: '',
HostPort: '2345',
},
],
'2367/udp': [
{
HostIp: '',
HostPort: '2346',
},
],
});
expect(ports.exposedPorts).to.deep.equal({
'1000/tcp': {},
'243/udp': {},
'2344/tcp': {},
'2354/tcp': {},
'2367/udp': {},
'53/tcp': {},
'53/udp': {},
});
});
it('correctly handles port ranges', () => {
const s = Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
releaseId: 2,
serviceId: 3,
imageId: 4,
expose: [1000, '243/udp'],
ports: ['1000-1003:2000-2003'],
},
{ appName: 'test' } as any,
);
const ports = (s as any).generateExposeAndPorts();
expect(ports.portBindings).to.deep.equal({
'2000/tcp': [
{
HostIp: '',
HostPort: '1000',
},
],
'2001/tcp': [
{
HostIp: '',
HostPort: '1001',
},
],
'2002/tcp': [
{
HostIp: '',
HostPort: '1002',
},
],
'2003/tcp': [
{
HostIp: '',
HostPort: '1003',
},
],
});
expect(ports.exposedPorts).to.deep.equal({
'1000/tcp': {},
'2000/tcp': {},
'2001/tcp': {},
'2002/tcp': {},
'2003/tcp': {},
'243/udp': {},
});
});
it('should correctly handle large port ranges', function() {
this.timeout(60000);
const s = Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
releaseId: 2,
serviceId: 3,
imageId: 4,
ports: ['5-65536:5-65536/tcp', '5-65536:5-65536/udp'],
},
{ appName: 'test' } as any,
);
expect((s as any).generateExposeAndPorts()).to.not.throw;
});
it('should correctly report implied exposed ports from portMappings', () => {
const service = Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
serviceName: 'test',
ports: ['80:80', '100:100'],
},
{ appName: 'test' } as any,
);
expect(service.config)
.to.have.property('expose')
.that.deep.equals(['80/tcp', '100/tcp']);
});
describe('Ordered array parameters', () => {
it('Should correctly compare ordered array parameters', () => {
const svc1 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
dns: ['8.8.8.8', '1.1.1.1'],
},
{ appName: 'test' } as any,
);
let svc2 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
dns: ['8.8.8.8', '1.1.1.1'],
},
{ appName: 'test' } as any,
);
assert(svc1.isEqualConfig(svc2, {}));
svc2 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
dns: ['1.1.1.1', '8.8.8.8'],
},
{ appName: 'test' } as any,
);
assert(!svc1.isEqualConfig(svc2, {}));
});
it('should correctly compare non-ordered array parameters', () => {
const svc1 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: ['abcdef', 'ghijk'],
},
{ appName: 'test' } as any,
);
let svc2 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: ['abcdef', 'ghijk'],
},
{ appName: 'test' } as any,
);
assert(svc1.isEqualConfig(svc2, {}));
svc2 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: ['ghijk', 'abcdef'],
},
{ appName: 'test' } as any,
);
assert(svc1.isEqualConfig(svc2, {}));
});
it('should correctly compare both ordered and non-ordered array parameters', () => {
const svc1 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: ['abcdef', 'ghijk'],
dns: ['8.8.8.8', '1.1.1.1'],
},
{ appName: 'test' } as any,
);
const svc2 = Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
serviceName: 'test',
volumes: ['ghijk', 'abcdef'],
dns: ['8.8.8.8', '1.1.1.1'],
},
{ appName: 'test' } as any,
);
assert(svc1.isEqualConfig(svc2, {}));
});
});
describe('parseMemoryNumber()', () => {
const makeComposeServiceWithLimit = (memLimit?: string | number) =>
Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
serviceName: 'foobar',
mem_limit: memLimit,
},
{ appName: 'test' } as any,
);
it('should correctly parse memory number strings without a unit', () =>
expect(makeComposeServiceWithLimit('64').config.memLimit).to.equal(64));
it('should correctly apply the default value', () =>
expect(makeComposeServiceWithLimit(undefined).config.memLimit).to.equal(
0,
));
it('should correctly support parsing numbers as memory limits', () =>
expect(makeComposeServiceWithLimit(64).config.memLimit).to.equal(64));
it('should correctly parse memory number strings that use a byte unit', () => {
expect(makeComposeServiceWithLimit('64b').config.memLimit).to.equal(64);
expect(makeComposeServiceWithLimit('64B').config.memLimit).to.equal(64);
});
it('should correctly parse memory number strings that use a kilobyte unit', () => {
expect(makeComposeServiceWithLimit('64k').config.memLimit).to.equal(
65536,
);
expect(makeComposeServiceWithLimit('64K').config.memLimit).to.equal(
65536,
);
expect(makeComposeServiceWithLimit('64kb').config.memLimit).to.equal(
65536,
);
expect(makeComposeServiceWithLimit('64Kb').config.memLimit).to.equal(
65536,
);
});
it('should correctly parse memory number strings that use a megabyte unit', () => {
expect(makeComposeServiceWithLimit('64m').config.memLimit).to.equal(
67108864,
);
expect(makeComposeServiceWithLimit('64M').config.memLimit).to.equal(
67108864,
);
expect(makeComposeServiceWithLimit('64mb').config.memLimit).to.equal(
67108864,
);
expect(makeComposeServiceWithLimit('64Mb').config.memLimit).to.equal(
67108864,
);
});
it('should correctly parse memory number strings that use a gigabyte unit', () => {
expect(makeComposeServiceWithLimit('64g').config.memLimit).to.equal(
68719476736,
);
expect(makeComposeServiceWithLimit('64G').config.memLimit).to.equal(
68719476736,
);
expect(makeComposeServiceWithLimit('64gb').config.memLimit).to.equal(
68719476736,
);
expect(makeComposeServiceWithLimit('64Gb').config.memLimit).to.equal(
68719476736,
);
});
});
describe('getWorkingDir', () => {
const makeComposeServiceWithWorkdir = (workdir?: string) =>
Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
serviceName: 'foobar',
workingDir: workdir,
},
{ appName: 'test' } as any,
);
it('should remove a trailing slash', () => {
expect(
makeComposeServiceWithWorkdir('/usr/src/app/').config.workingDir,
).to.equal('/usr/src/app');
expect(makeComposeServiceWithWorkdir('/').config.workingDir).to.equal(
'/',
);
expect(
makeComposeServiceWithWorkdir('/usr/src/app').config.workingDir,
).to.equal('/usr/src/app');
expect(makeComposeServiceWithWorkdir('').config.workingDir).to.equal('');
});
});
describe('Docker <-> Compose config', () => {
const omitConfigForComparison = (config: ServiceConfig) =>
_.omit(config, ['running', 'networks']);
it('should be identical when converting a simple service', () => {
const composeSvc = Service.fromComposeObject(
configs.simple.compose,
configs.simple.imageInfo,
);
const dockerSvc = Service.fromDockerContainer(configs.simple.inspect);
const composeConfig = omitConfigForComparison(composeSvc.config);
const dockerConfig = omitConfigForComparison(dockerSvc.config);
expect(composeConfig).to.deep.equal(dockerConfig);
expect(dockerSvc.isEqualConfig(composeSvc, {})).to.be.true;
});
it('should correct convert formats with a null entrypoint', () => {
const composeSvc = Service.fromComposeObject(
configs.entrypoint.compose,
configs.entrypoint.imageInfo,
);
const dockerSvc = Service.fromDockerContainer(configs.entrypoint.inspect);
const composeConfig = omitConfigForComparison(composeSvc.config);
const dockerConfig = omitConfigForComparison(dockerSvc.config);
expect(composeConfig).to.deep.equal(dockerConfig);
expect(dockerSvc.isEqualConfig(composeSvc, {})).to.equals(true);
});
describe('Networks', () => {
it('should correctly convert networks from compose to docker format', () => {
const makeComposeServiceWithNetwork = (
networks: ServiceComposeConfig['networks'],
) =>
Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
serviceName: 'test',
networks,
},
{ appName: 'test' } as any,
);
expect(
makeComposeServiceWithNetwork({
balena: {
ipv4Address: '1.2.3.4',
},
}).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
).to.deep.equal({
EndpointsConfig: {
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4',
},
Aliases: ['test'],
},
},
});
expect(
makeComposeServiceWithNetwork({
balena: {
aliases: ['test', '1123'],
ipv4Address: '1.2.3.4',
ipv6Address: '5.6.7.8',
linkLocalIps: ['123.123.123'],
},
}).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
).to.deep.equal({
EndpointsConfig: {
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4',
IPv6Address: '5.6.7.8',
LinkLocalIPs: ['123.123.123'],
},
Aliases: ['test', '1123'],
},
},
});
});
it('should correctly convert Docker format to service format', () => {
const dockerCfg = require('./data/docker-states/simple/inspect.json');
const makeServiceFromDockerWithNetwork = (networks: {
[name: string]: any;
}) => {
const newConfig = _.cloneDeep(dockerCfg);
newConfig.NetworkSettings = {
Networks: networks,
};
return Service.fromDockerContainer(newConfig);
};
expect(
makeServiceFromDockerWithNetwork({
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4',
},
Aliases: [],
},
}).config.networks,
).to.deep.equal({
'123456_balena': {
ipv4Address: '1.2.3.4',
},
});
expect(
makeServiceFromDockerWithNetwork({
'123456_balena': {
IPAMConfig: {
IPv4Address: '1.2.3.4',
IPv6Address: '5.6.7.8',
LinkLocalIps: ['123.123.123'],
},
Aliases: ['test', '1123'],
},
}).config.networks,
).to.deep.equal({
'123456_balena': {
ipv4Address: '1.2.3.4',
ipv6Address: '5.6.7.8',
linkLocalIps: ['123.123.123'],
aliases: ['test', '1123'],
},
});
});
});
return describe('Network mode=service:', () => {
it('should correctly add a depends_on entry for the service', () => {
let s = Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
releaseId: 2,
serviceId: 3,
imageId: 4,
network_mode: 'service: test',
},
{ appName: 'test' } as any,
);
expect(s.dependsOn).to.deep.equal(['test']);
s = Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
releaseId: 2,
serviceId: 3,
imageId: 4,
depends_on: ['another_service'],
network_mode: 'service: test',
},
{ appName: 'test' } as any,
);
expect(s.dependsOn).to.deep.equal(['another_service', 'test']);
});
it('should correctly convert a network_mode service: to a container:', () => {
const s = Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
releaseId: 2,
serviceId: 3,
imageId: 4,
network_mode: 'service: test',
},
{ appName: 'test' } as any,
);
return expect(
s.toDockerContainer({
deviceName: '',
containerIds: { test: 'abcdef' },
}),
)
.to.have.property('HostConfig')
.that.has.property('NetworkMode')
.that.equals('container:abcdef');
});
it('should not cause a container restart if a service: container has not changed', () => {
const composeSvc = Service.fromComposeObject(
configs.networkModeService.compose,
configs.networkModeService.imageInfo,
);
const dockerSvc = Service.fromDockerContainer(
configs.networkModeService.inspect,
);
const composeConfig = omitConfigForComparison(composeSvc.config);
const dockerConfig = omitConfigForComparison(dockerSvc.config);
expect(composeConfig).to.not.deep.equal(dockerConfig);
expect(dockerSvc.isEqualConfig(composeSvc, { test: 'abcdef' })).to.be
.true;
});
it('should restart a container when its dependent network mode container changes', () => {
const composeSvc = Service.fromComposeObject(
configs.networkModeService.compose,
configs.networkModeService.imageInfo,
);
const dockerSvc = Service.fromDockerContainer(
configs.networkModeService.inspect,
);
const composeConfig = omitConfigForComparison(composeSvc.config);
const dockerConfig = omitConfigForComparison(dockerSvc.config);
expect(composeConfig).to.not.deep.equal(dockerConfig);
return expect(dockerSvc.isEqualConfig(composeSvc, { test: 'qwerty' }))
.to.be.false;
});
});
});
});

View File

@ -1,311 +0,0 @@
Promise = require 'bluebird'
_ = require 'lodash'
{ stub } = require 'sinon'
chai = require './lib/chai-config'
chai.use(require('chai-events'))
{ expect } = chai
prepare = require './lib/prepare'
DeviceState = require '../src/device-state'
{ DB } = require('../src/db')
{ Config } = require('../src/config')
{ RPiConfigBackend } = require('../src/config/backend')
{ Service } = require '../src/compose/service'
mockedInitialConfig = {
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
'RESIN_SUPERVISOR_DELTA': 'false'
'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': '0'
'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
'RESIN_SUPERVISOR_DELTA_VERSION': '2'
'RESIN_SUPERVISOR_INSTANT_UPDATE_TRIGGER': 'true'
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
}
testTarget1 = {
local: {
name: 'aDevice'
config: {
'HOST_CONFIG_gpu_mem': '256'
'SUPERVISOR_CONNECTIVITY_CHECK': 'true'
'SUPERVISOR_DELTA': 'false'
'SUPERVISOR_DELTA_APPLY_TIMEOUT': '0'
'SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
'SUPERVISOR_DELTA_RETRY_COUNT': '30'
'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
'SUPERVISOR_DELTA_VERSION': '2'
'SUPERVISOR_INSTANT_UPDATE_TRIGGER': 'true'
'SUPERVISOR_LOCAL_MODE': 'false'
'SUPERVISOR_LOG_CONTROL': 'true'
'SUPERVISOR_OVERRIDE_LOCK': 'false'
'SUPERVISOR_POLL_INTERVAL': '60000'
'SUPERVISOR_VPN_CONTROL': 'true'
'SUPERVISOR_PERSISTENT_LOGGING': 'false'
}
apps: {
'1234': {
appId: 1234
name: 'superapp'
commit: 'abcdef'
releaseId: 1
services: [
{
appId: 1234
serviceId: 23
imageId: 12345
serviceName: 'someservice'
releaseId: 1
image: 'registry2.resin.io/superapp/abcdef:latest'
labels: {
'io.resin.something': 'bar'
}
}
]
volumes: {}
networks: {}
}
}
}
dependent: { apps: [], devices: [] }
}
testTarget2 = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
}
apps: {
'1234': {
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: {
'23': {
serviceName: 'aservice'
imageId: 12345
image: 'registry2.resin.io/superapp/edfabc'
environment: {
'FOO': 'bar'
}
labels: {}
},
'24': {
serviceName: 'anotherService'
imageId: 12346
image: 'registry2.resin.io/superapp/afaff'
environment: {
'FOO': 'bro'
}
labels: {}
}
}
}
}
}
dependent: { apps: [], devices: [] }
}
testTargetWithDefaults2 = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'HOST_CONFIG_gpu_mem': '512'
'SUPERVISOR_CONNECTIVITY_CHECK': 'true'
'SUPERVISOR_DELTA': 'false'
'SUPERVISOR_DELTA_APPLY_TIMEOUT': '0'
'SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
'SUPERVISOR_DELTA_RETRY_COUNT': '30'
'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
'SUPERVISOR_DELTA_VERSION': '2'
'SUPERVISOR_INSTANT_UPDATE_TRIGGER': 'true'
'SUPERVISOR_LOCAL_MODE': 'false'
'SUPERVISOR_LOG_CONTROL': 'true'
'SUPERVISOR_OVERRIDE_LOCK': 'false'
'SUPERVISOR_POLL_INTERVAL': '60000'
'SUPERVISOR_VPN_CONTROL': 'true'
'SUPERVISOR_PERSISTENT_LOGGING': 'false'
}
apps: {
'1234': {
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: [
_.merge({ appId: 1234, serviceId: 23, releaseId: 2 }, _.clone(testTarget2.local.apps['1234'].services['23'])),
_.merge({ appId: 1234, serviceId: 24, releaseId: 2 }, _.clone(testTarget2.local.apps['1234'].services['24']))
]
volumes: {}
networks: {}
}
}
}
dependent: { apps: [], devices: [] }
}
testTargetInvalid = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
}
apps: [
{
appId: '1234'
name: 'superapp'
commit: 'afafafa'
releaseId: '2'
config: {}
services: [
{
serviceId: '23'
serviceName: 'aservice'
imageId: '12345'
image: 'registry2.resin.io/superapp/edfabc'
config: {}
environment: {
' FOO': 'bar'
}
labels: {}
},
{
serviceId: '24'
serviceName: 'anotherService'
imageId: '12346'
image: 'registry2.resin.io/superapp/afaff'
config: {}
environment: {
'FOO': 'bro'
}
labels: {}
}
]
}
]
}
dependent: { apps: [], devices: [] }
}
describe 'deviceState', ->
before ->
prepare()
@db = new DB()
@config = new Config({ @db })
@logger = {
clearOutOfDateDBLogs: ->
}
eventTracker = {
track: console.log
}
stub(Service, 'extendEnvVars').callsFake (env) ->
env['ADDITIONAL_ENV_VAR'] = 'foo'
return env
@deviceState = new DeviceState({ @db, @config, eventTracker, @logger })
stub(@deviceState.applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1'))
stub(@deviceState.applications.images, 'inspectByName').callsFake ->
Promise.try ->
err = new Error()
err.statusCode = 404
throw err
@deviceState.deviceConfig.configBackend = new RPiConfigBackend()
@db.init()
.then =>
@config.init()
after ->
Service.extendEnvVars.restore()
@deviceState.applications.docker.getNetworkGateway.restore()
@deviceState.applications.images.inspectByName.restore()
it 'loads a target state from an apps.json file and saves it as target state, then returns it', ->
stub(@deviceState.applications.images, 'save').returns(Promise.resolve())
stub(@deviceState.deviceConfig, 'getCurrent').returns(Promise.resolve(mockedInitialConfig))
@deviceState.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json')
.then =>
@deviceState.getTarget()
.then (targetState) ->
testTarget = _.cloneDeep(testTarget1)
testTarget.local.apps['1234'].services = _.map testTarget.local.apps['1234'].services, (s) ->
s.imageName = s.image
return Service.fromComposeObject(s, { appName: 'superapp' })
# We serialize and parse JSON to avoid checking fields that are functions or undefined
expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(JSON.parse(JSON.stringify(testTarget)))
.finally =>
@deviceState.applications.images.save.restore()
@deviceState.deviceConfig.getCurrent.restore()
it 'stores info for pinning a device after loading an apps.json with a pinDevice field', ->
stub(@deviceState.applications.images, 'save').returns(Promise.resolve())
stub(@deviceState.deviceConfig, 'getCurrent').returns(Promise.resolve(mockedInitialConfig))
@deviceState.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps-pin.json')
.then =>
@deviceState.applications.images.save.restore()
@deviceState.deviceConfig.getCurrent.restore()
@config.get('pinDevice').then (pinned) ->
expect(pinned).to.have.property('app').that.equals(1234)
expect(pinned).to.have.property('commit').that.equals('abcdef')
it 'emits a change event when a new state is reported', ->
@deviceState.reportCurrentState({ someStateDiff: 'someValue' })
expect(@deviceState).to.emit('change')
it 'returns the current state'
it 'writes the target state to the db with some extra defaults', ->
testTarget = _.cloneDeep(testTargetWithDefaults2)
Promise.map testTarget.local.apps['1234'].services, (s) =>
@deviceState.applications.images.normalise(s.image)
.then (imageName) ->
s.image = imageName
s.imageName = imageName
Service.fromComposeObject(s, { appName: 'supertest' })
.then (services) =>
testTarget.local.apps['1234'].services = services
@deviceState.setTarget(testTarget2)
.then =>
@deviceState.getTarget()
.then (target) ->
expect(JSON.parse(JSON.stringify(target))).to.deep.equal(JSON.parse(JSON.stringify(testTarget)))
it 'does not allow setting an invalid target state', ->
promise = @deviceState.setTarget(testTargetInvalid)
promise.catch(->)
expect(promise).to.be.rejected
it 'allows triggering applying the target state', (done) ->
stub(@deviceState, 'applyTarget').returns(Promise.resolve())
@deviceState.triggerApplyTarget({ force: true })
expect(@deviceState.applyTarget).to.not.be.called
setTimeout =>
expect(@deviceState.applyTarget).to.be.calledWith({ force: true, initial: false })
@deviceState.applyTarget.restore()
done()
, 5
it 'cancels current promise applying the target state', (done) ->
@deviceState.scheduledApply = { force: false, delay: 100 }
@deviceState.applyInProgress = true
@deviceState.applyCancelled = false
new Promise (resolve, reject) =>
setTimeout(resolve, 100000)
@deviceState.cancelDelay = reject
.catch =>
@deviceState.applyCancelled = true
.finally =>
expect(@deviceState.scheduledApply).to.deep.equal({ force: true, delay: 0 })
expect(@deviceState.applyCancelled).to.be.true
done()
@deviceState.triggerApplyTarget({ force: true, isFromApi: true })
it 'applies the target state for device config'
it 'applies the target state for applications'

View File

@ -0,0 +1,384 @@
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import { stub } from 'sinon';
import chai = require('./lib/chai-config');
import prepare = require('./lib/prepare');
// tslint:disable-next-line
chai.use(require('chai-events'));
const { expect } = chai;
import Config from '../src/config';
import { RPiConfigBackend } from '../src/config/backend';
import DB from '../src/db';
import DeviceState = require('../src/device-state');
import Service from '../src/compose/service';
const mockedInitialConfig = {
RESIN_SUPERVISOR_CONNECTIVITY_CHECK: 'true',
RESIN_SUPERVISOR_DELTA: 'false',
RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT: '30000',
RESIN_SUPERVISOR_DELTA_RETRY_COUNT: '30',
RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
RESIN_SUPERVISOR_DELTA_VERSION: '2',
RESIN_SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
RESIN_SUPERVISOR_LOCAL_MODE: 'false',
RESIN_SUPERVISOR_LOG_CONTROL: 'true',
RESIN_SUPERVISOR_OVERRIDE_LOCK: 'false',
RESIN_SUPERVISOR_POLL_INTERVAL: '60000',
RESIN_SUPERVISOR_VPN_CONTROL: 'true',
};
const testTarget1 = {
local: {
name: 'aDevice',
config: {
HOST_CONFIG_gpu_mem: '256',
SUPERVISOR_CONNECTIVITY_CHECK: 'true',
SUPERVISOR_DELTA: 'false',
SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
SUPERVISOR_DELTA_REQUEST_TIMEOUT: '30000',
SUPERVISOR_DELTA_RETRY_COUNT: '30',
SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
SUPERVISOR_DELTA_VERSION: '2',
SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
SUPERVISOR_LOCAL_MODE: 'false',
SUPERVISOR_LOG_CONTROL: 'true',
SUPERVISOR_OVERRIDE_LOCK: 'false',
SUPERVISOR_POLL_INTERVAL: '60000',
SUPERVISOR_VPN_CONTROL: 'true',
SUPERVISOR_PERSISTENT_LOGGING: 'false',
},
apps: {
'1234': {
appId: 1234,
name: 'superapp',
commit: 'abcdef',
releaseId: 1,
services: [
{
appId: 1234,
serviceId: 23,
imageId: 12345,
serviceName: 'someservice',
releaseId: 1,
image: 'registry2.resin.io/superapp/abcdef:latest',
labels: {
'io.resin.something': 'bar',
},
},
],
volumes: {},
networks: {},
},
},
},
dependent: { apps: [], devices: [] },
};
const testTarget2 = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
},
apps: {
'1234': {
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: {
'23': {
serviceName: 'aservice',
imageId: 12345,
image: 'registry2.resin.io/superapp/edfabc',
environment: {
FOO: 'bar',
},
labels: {},
},
'24': {
serviceName: 'anotherService',
imageId: 12346,
image: 'registry2.resin.io/superapp/afaff',
environment: {
FOO: 'bro',
},
labels: {},
},
},
},
},
},
dependent: { apps: [], devices: [] },
};
const testTargetWithDefaults2 = {
local: {
name: 'aDeviceWithDifferentName',
config: {
HOST_CONFIG_gpu_mem: '512',
SUPERVISOR_CONNECTIVITY_CHECK: 'true',
SUPERVISOR_DELTA: 'false',
SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
SUPERVISOR_DELTA_REQUEST_TIMEOUT: '30000',
SUPERVISOR_DELTA_RETRY_COUNT: '30',
SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
SUPERVISOR_DELTA_VERSION: '2',
SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
SUPERVISOR_LOCAL_MODE: 'false',
SUPERVISOR_LOG_CONTROL: 'true',
SUPERVISOR_OVERRIDE_LOCK: 'false',
SUPERVISOR_POLL_INTERVAL: '60000',
SUPERVISOR_VPN_CONTROL: 'true',
SUPERVISOR_PERSISTENT_LOGGING: 'false',
},
apps: {
'1234': {
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: [
_.merge(
{ appId: 1234, serviceId: 23, releaseId: 2 },
_.clone(testTarget2.local.apps['1234'].services['23']),
),
_.merge(
{ appId: 1234, serviceId: 24, releaseId: 2 },
_.clone(testTarget2.local.apps['1234'].services['24']),
),
],
volumes: {},
networks: {},
},
},
},
dependent: { apps: [], devices: [] },
};
const testTargetInvalid = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
},
apps: [
{
appId: '1234',
name: 'superapp',
commit: 'afafafa',
releaseId: '2',
config: {},
services: [
{
serviceId: '23',
serviceName: 'aservice',
imageId: '12345',
image: 'registry2.resin.io/superapp/edfabc',
config: {},
environment: {
' FOO': 'bar',
},
labels: {},
},
{
serviceId: '24',
serviceName: 'anotherService',
imageId: '12346',
image: 'registry2.resin.io/superapp/afaff',
config: {},
environment: {
FOO: 'bro',
},
labels: {},
},
],
},
],
},
dependent: { apps: [], devices: [] },
};
describe('deviceState', () => {
const db = new DB();
const config = new Config({ db });
const logger = {
clearOutOfDateDBLogs() {
/* noop */
},
};
let deviceState: DeviceState;
before(async () => {
prepare();
const eventTracker = {
track: console.log,
};
stub(Service as any, 'extendEnvVars').callsFake(env => {
env['ADDITIONAL_ENV_VAR'] = 'foo';
return env;
});
deviceState = new DeviceState({
db,
config,
eventTracker: eventTracker as any,
logger: logger as any,
});
stub(deviceState.applications.docker, 'getNetworkGateway').returns(
Promise.resolve('172.17.0.1'),
);
stub(deviceState.applications.images, 'inspectByName').callsFake(() => {
const err: any = new Error();
err.statusCode = 404;
return Promise.reject(err);
});
(deviceState as any).deviceConfig.configBackend = new RPiConfigBackend();
await db.init();
await config.init();
});
after(() => {
(Service as any).extendEnvVars.restore();
(deviceState.applications.docker
.getNetworkGateway as sinon.SinonStub).restore();
(deviceState.applications.images
.inspectByName as sinon.SinonStub).restore();
});
it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => {
stub(deviceState.applications.images, 'save').returns(Promise.resolve());
stub(deviceState.deviceConfig, 'getCurrent').returns(
Promise.resolve(mockedInitialConfig),
);
try {
await deviceState.loadTargetFromFile(
process.env.ROOT_MOUNTPOINT + '/apps.json',
);
const targetState = await deviceState.getTarget();
const testTarget = _.cloneDeep(testTarget1);
testTarget.local.apps['1234'].services = _.map(
testTarget.local.apps['1234'].services,
(s: any) => {
s.imageName = s.image;
return Service.fromComposeObject(s, { appName: 'superapp' } as any);
},
) as any;
expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(
JSON.parse(JSON.stringify(testTarget)),
);
} finally {
(deviceState.applications.images.save as sinon.SinonStub).restore();
(deviceState.deviceConfig.getCurrent as sinon.SinonStub).restore();
}
});
it('stores info for pinning a device after loading an apps.json with a pinDevice field', () => {
stub(deviceState.applications.images, 'save').returns(Promise.resolve());
stub(deviceState.deviceConfig, 'getCurrent').returns(
Promise.resolve(mockedInitialConfig),
);
deviceState
.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps-pin.json')
.then(() => {
(deviceState as any).applications.images.save.restore();
(deviceState as any).deviceConfig.getCurrent.restore();
config.get('pinDevice').then(pinned => {
expect(pinned)
.to.have.property('app')
.that.equals(1234);
expect(pinned)
.to.have.property('commit')
.that.equals('abcdef');
});
});
});
it('emits a change event when a new state is reported', () => {
deviceState.reportCurrentState({ someStateDiff: 'someValue' });
return (expect as any)(deviceState).to.emit('change');
});
it('returns the current state');
it('writes the target state to the db with some extra defaults', async () => {
const testTarget = _.cloneDeep(testTargetWithDefaults2);
const services: Service[] = [];
for (const service of testTarget.local.apps['1234'].services) {
const imageName = await (deviceState.applications
.images as any).normalise(service.image);
service.image = imageName;
(service as any).imageName = imageName;
services.push(
Service.fromComposeObject(service, { appName: 'supertest' } as any),
);
}
(testTarget as any).local.apps['1234'].services = services;
await deviceState.setTarget(testTarget2);
const target = await deviceState.getTarget();
expect(JSON.parse(JSON.stringify(target))).to.deep.equal(
JSON.parse(JSON.stringify(testTarget)),
);
});
it('does not allow setting an invalid target state', () => {
expect(deviceState.setTarget(testTargetInvalid)).to.be.rejected;
});
it('allows triggering applying the target state', done => {
stub(deviceState as any, 'applyTarget').returns(Promise.resolve());
deviceState.triggerApplyTarget({ force: true });
expect((deviceState as any).applyTarget).to.not.be.called;
setTimeout(() => {
expect((deviceState as any).applyTarget).to.be.calledWith({
force: true,
initial: false,
});
(deviceState as any).applyTarget.restore();
done();
}, 5);
});
it('cancels current promise applying the target state', done => {
(deviceState as any).scheduledApply = { force: false, delay: 100 };
(deviceState as any).applyInProgress = true;
(deviceState as any).applyCancelled = false;
new Bluebird((resolve, reject) => {
setTimeout(resolve, 100000);
(deviceState as any).cancelDelay = reject;
})
.catch(() => {
(deviceState as any).applyCancelled = true;
})
.finally(() => {
expect((deviceState as any).scheduledApply).to.deep.equal({
force: true,
delay: 0,
});
expect((deviceState as any).applyCancelled).to.be.true;
done();
});
deviceState.triggerApplyTarget({ force: true, isFromApi: true });
});
it('applies the target state for device config');
it('applies the target state for applications');
});

View File

@ -1,54 +0,0 @@
Promise = require 'bluebird'
iptables = require '../src/lib/iptables'
{ stub } = require 'sinon'
{ expect } = require './lib/chai-config'
describe 'iptables', ->
it 'calls iptables to delete and recreate rules to block a port', ->
stub(iptables, 'execAsync').returns(Promise.resolve())
iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42)
.then ->
expect(iptables.execAsync.callCount).to.equal(12)
expect(iptables.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j REJECT')
expect(iptables.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j REJECT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -D INPUT -p tcp --dport 42 -j REJECT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -A INPUT -p tcp --dport 42 -j REJECT')
.then ->
iptables.execAsync.restore()
it "falls back to blocking the port with DROP if there's no REJECT support", ->
stub(iptables, 'execAsync').callsFake (cmd) ->
if /REJECT$/.test(cmd)
Promise.reject(new Error())
else
Promise.resolve()
iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42)
.then ->
expect(iptables.execAsync.callCount).to.equal(16)
expect(iptables.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j REJECT')
expect(iptables.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j REJECT')
expect(iptables.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j DROP')
expect(iptables.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j DROP')
expect(iptables.execAsync).to.be.calledWith('ip6tables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -D INPUT -p tcp --dport 42 -j REJECT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -A INPUT -p tcp --dport 42 -j REJECT')
expect(iptables.execAsync).to.be.calledWith('ip6tables -D INPUT -p tcp --dport 42 -j DROP')
expect(iptables.execAsync).to.be.calledWith('ip6tables -A INPUT -p tcp --dport 42 -j DROP')
.then ->
iptables.execAsync.restore()

114
test/06-iptables.spec.ts Normal file
View File

@ -0,0 +1,114 @@
import * as Bluebird from 'bluebird';
import { stub } from 'sinon';
import { expect } from './lib/chai-config';
import * as iptables from '../src/lib/iptables';
describe('iptables', async () => {
it('calls iptables to delete and recreate rules to block a port', async () => {
stub(iptables, 'execAsync').returns(Bluebird.resolve(''));
await iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42);
expect((iptables.execAsync as sinon.SinonStub).callCount).to.equal(12);
expect(iptables.execAsync).to.be.calledWith(
'iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -D INPUT -p tcp --dport 42 -j REJECT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -A INPUT -p tcp --dport 42 -j REJECT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -D INPUT -p tcp --dport 42 -j REJECT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -A INPUT -p tcp --dport 42 -j REJECT',
);
(iptables.execAsync as sinon.SinonStub).restore();
});
it("falls back to blocking the port with DROP if there's no REJECT support", async () => {
stub(iptables, 'execAsync').callsFake(cmd => {
if (/REJECT$/.test(cmd)) {
return Bluebird.reject(new Error());
} else {
return Bluebird.resolve('');
}
});
await iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42);
expect((iptables.execAsync as sinon.SinonStub).callCount).to.equal(16);
expect(iptables.execAsync).to.be.calledWith(
'iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -D INPUT -p tcp --dport 42 -j REJECT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -A INPUT -p tcp --dport 42 -j REJECT',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -D INPUT -p tcp --dport 42 -j DROP',
);
expect(iptables.execAsync).to.be.calledWith(
'iptables -A INPUT -p tcp --dport 42 -j DROP',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -D INPUT -p tcp --dport 42 -j REJECT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -A INPUT -p tcp --dport 42 -j REJECT',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -D INPUT -p tcp --dport 42 -j DROP',
);
expect(iptables.execAsync).to.be.calledWith(
'ip6tables -A INPUT -p tcp --dport 42 -j DROP',
);
(iptables.execAsync as sinon.SinonStub).restore();
});
});

View File

@ -1,218 +0,0 @@
_ = require 'lodash'
{ expect } = require './lib/chai-config'
validation = require '../src/lib/validation'
almostTooLongText = _.map([0...255], -> 'a').join('')
describe 'validation', ->
describe 'checkTruthy', ->
it 'returns true for a truthy value', ->
expect(validation.checkTruthy(true)).to.equal(true)
expect(validation.checkTruthy('true')).to.equal(true)
expect(validation.checkTruthy('1')).to.equal(true)
expect(validation.checkTruthy(1)).to.equal(true)
expect(validation.checkTruthy('on')).to.equal(true)
it 'returns false for a falsy value', ->
expect(validation.checkTruthy(false)).to.equal(false)
expect(validation.checkTruthy('false')).to.equal(false)
expect(validation.checkTruthy('0')).to.equal(false)
expect(validation.checkTruthy(0)).to.equal(false)
expect(validation.checkTruthy('off')).to.equal(false)
it 'returns undefined for invalid values', ->
expect(validation.checkTruthy({})).to.be.undefined
expect(validation.checkTruthy(10)).to.be.undefined
expect(validation.checkTruthy('on1')).to.be.undefined
expect(validation.checkTruthy('foo')).to.be.undefined
expect(validation.checkTruthy(undefined)).to.be.undefined
expect(validation.checkTruthy(null)).to.be.undefined
expect(validation.checkTruthy('')).to.be.undefined
describe 'checkString', ->
it 'validates a string', ->
expect(validation.checkString('foo')).to.equal('foo')
expect(validation.checkString('bar')).to.equal('bar')
it 'returns undefined for empty strings or strings that equal null or undefined', ->
expect(validation.checkString('')).to.be.undefined
expect(validation.checkString('null')).to.be.undefined
expect(validation.checkString('undefined')).to.be.undefined
it 'returns undefined for things that are not strings', ->
expect(validation.checkString({})).to.be.undefined
expect(validation.checkString([])).to.be.undefined
expect(validation.checkString(123)).to.be.undefined
expect(validation.checkString(0)).to.be.undefined
expect(validation.checkString(null)).to.be.undefined
expect(validation.checkString(undefined)).to.be.undefined
describe 'checkInt', ->
it 'returns an integer for a string that can be parsed as one', ->
expect(validation.checkInt('200')).to.equal(200)
expect(validation.checkInt('0')).to.equal(0)
expect(validation.checkInt('-3')).to.equal(-3)
it 'returns the same integer when passed an integer', ->
expect(validation.checkInt(345)).to.equal(345)
expect(validation.checkInt(-345)).to.equal(-345)
it 'returns undefined when passed something that can\'t be parsed as int', ->
expect(validation.checkInt({})).to.be.undefined
expect(validation.checkInt([])).to.be.undefined
expect(validation.checkInt('foo')).to.be.undefined
expect(validation.checkInt(null)).to.be.undefined
expect(validation.checkInt(undefined)).to.be.undefined
it 'returns undefined when passed a negative or zero value and the positive option is set', ->
expect(validation.checkInt('-3', positive: true)).to.be.undefined
expect(validation.checkInt('0', positive: true)).to.be.undefined
describe 'isValidShortText', ->
it 'returns true for a short text', ->
expect(validation.isValidShortText('foo')).to.equal(true)
expect(validation.isValidShortText('')).to.equal(true)
expect(validation.isValidShortText(almostTooLongText)).to.equal(true)
it 'returns false for a text longer than 255 characters', ->
expect(validation.isValidShortText(almostTooLongText + 'a')).to.equal(false)
it 'returns false when passed a non-string', ->
expect(validation.isValidShortText({})).to.equal(false)
expect(validation.isValidShortText(1)).to.equal(false)
expect(validation.isValidShortText(null)).to.equal(false)
expect(validation.isValidShortText(undefined)).to.equal(false)
describe 'isValidAppsObject', ->
it 'returns true for a valid object', ->
apps = {
'1234': {
name: 'something'
releaseId: 123
commit: 'bar'
services: {
'45': {
serviceName: 'bazbaz'
imageId: 34
image: 'foo'
environment: {}
labels: {}
}
}
}
}
expect(validation.isValidAppsObject(apps)).to.equal(true)
it 'returns false with an invalid environment', ->
apps = {
'1234': {
name: 'something'
releaseId: 123
commit: 'bar'
services: {
'45': {
serviceName: 'bazbaz'
imageId: 34
image: 'foo'
environment: { ' baz': 'bat' }
labels: {}
}
}
}
}
expect(validation.isValidAppsObject(apps)).to.equal(false)
it 'returns false with an invalid appId', ->
apps = {
'boo': {
name: 'something'
releaseId: 123
commit: 'bar'
services: {
'45': {
serviceName: 'bazbaz'
imageId: 34
image: 'foo'
environment: {}
labels: {}
}
}
}
}
expect(validation.isValidAppsObject(apps)).to.equal(false)
it 'returns true with a missing releaseId', ->
apps = {
'1234': {
name: 'something'
services: {
'45': {
serviceName: 'bazbaz'
imageId: 34
image: 'foo'
environment: {}
labels: {}
}
}
}
}
expect(validation.isValidAppsObject(apps)).to.equal(true)
it 'returns false with an invalid releaseId', ->
apps = {
'1234': {
name: 'something'
releaseId: '123a'
services: {
'45': {
serviceName: 'bazbaz'
imageId: 34
image: 'foo'
environment: {}
labels: {}
}
}
}
}
expect(validation.isValidAppsObject(apps)).to.equal(true)
describe 'isValidDependentDevicesObject', ->
it 'returns true for a valid object', ->
devices = {}
devices[almostTooLongText] = {
name: 'foo'
apps: {
'234': {
config: { bar: 'baz' }
environment: { dead: 'beef' }
}
}
}
expect(validation.isValidDependentDevicesObject(devices)).to.equal(true)
it 'returns false with a missing apps object', ->
devices = {
'abcd1234': {
name: 'foo'
}
}
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false)
it 'returns false with an invalid environment', ->
devices = {
'abcd1234': {
name: 'foo'
apps: {
'234': {
config: { bar: 'baz' }
environment: { dead: 1 }
}
}
}
}
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false)
it 'returns false if the uuid is too long', ->
devices = {}
devices[almostTooLongText + 'a'] = {
name: 'foo'
apps: {
'234': {
config: { bar: 'baz' }
environment: { dead: 'beef' }
}
}
}
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false)

258
test/07-validation.spec.ts Normal file
View File

@ -0,0 +1,258 @@
import * as _ from 'lodash';
import { expect } from './lib/chai-config';
import * as validation from '../src/lib/validation';
const almostTooLongText = _.times(255, () => 'a').join('');
describe('validation', () => {
describe('checkTruthy', () => {
it('returns true for a truthy value', () => {
expect(validation.checkTruthy(true)).to.equal(true);
expect(validation.checkTruthy('true')).to.equal(true);
expect(validation.checkTruthy('1')).to.equal(true);
expect(validation.checkTruthy(1)).to.equal(true);
expect(validation.checkTruthy('on')).to.equal(true);
});
it('returns false for a falsy value', () => {
expect(validation.checkTruthy(false)).to.equal(false);
expect(validation.checkTruthy('false')).to.equal(false);
expect(validation.checkTruthy('0')).to.equal(false);
expect(validation.checkTruthy(0)).to.equal(false);
expect(validation.checkTruthy('off')).to.equal(false);
});
it('returns undefined for invalid values', () => {
expect(validation.checkTruthy({})).to.be.undefined;
expect(validation.checkTruthy(10)).to.be.undefined;
expect(validation.checkTruthy('on1')).to.be.undefined;
expect(validation.checkTruthy('foo')).to.be.undefined;
expect(validation.checkTruthy(undefined)).to.be.undefined;
expect(validation.checkTruthy(null)).to.be.undefined;
expect(validation.checkTruthy('')).to.be.undefined;
});
});
describe('checkString', () => {
it('validates a string', () => {
expect(validation.checkString('foo')).to.equal('foo');
expect(validation.checkString('bar')).to.equal('bar');
});
it('returns undefined for empty strings or strings that equal null or undefined', () => {
expect(validation.checkString('')).to.be.undefined;
expect(validation.checkString('null')).to.be.undefined;
expect(validation.checkString('undefined')).to.be.undefined;
});
it('returns undefined for things that are not strings', () => {
expect(validation.checkString({})).to.be.undefined;
expect(validation.checkString([])).to.be.undefined;
expect(validation.checkString(123)).to.be.undefined;
expect(validation.checkString(0)).to.be.undefined;
expect(validation.checkString(null)).to.be.undefined;
expect(validation.checkString(undefined)).to.be.undefined;
});
});
describe('checkInt', () => {
it('returns an integer for a string that can be parsed as one', () => {
expect(validation.checkInt('200')).to.equal(200);
expect(validation.checkInt('0')).to.equal(0);
expect(validation.checkInt('-3')).to.equal(-3);
});
it('returns the same integer when passed an integer', () => {
expect(validation.checkInt(345)).to.equal(345);
return expect(validation.checkInt(-345)).to.equal(-345);
});
it("returns undefined when passed something that can't be parsed as int", () => {
expect(validation.checkInt({})).to.be.undefined;
expect(validation.checkInt([])).to.be.undefined;
expect(validation.checkInt('foo')).to.be.undefined;
expect(validation.checkInt(null)).to.be.undefined;
return expect(validation.checkInt(undefined)).to.be.undefined;
});
it('returns undefined when passed a negative or zero value and the positive option is set', () => {
expect(validation.checkInt('-3', { positive: true })).to.be.undefined;
expect(validation.checkInt('0', { positive: true })).to.be.undefined;
});
});
describe('isValidShortText', () => {
it('returns true for a short text', () => {
expect(validation.isValidShortText('foo')).to.equal(true);
expect(validation.isValidShortText('')).to.equal(true);
expect(validation.isValidShortText(almostTooLongText)).to.equal(true);
});
it('returns false for a text longer than 255 characters', () =>
expect(validation.isValidShortText(almostTooLongText + 'a')).to.equal(
false,
));
it('returns false when passed a non-string', () => {
expect(validation.isValidShortText({})).to.equal(false);
expect(validation.isValidShortText(1)).to.equal(false);
expect(validation.isValidShortText(null)).to.equal(false);
expect(validation.isValidShortText(undefined)).to.equal(false);
});
});
describe('isValidAppsObject', () => {
it('returns true for a valid object', () => {
const apps = {
'1234': {
name: 'something',
releaseId: 123,
commit: 'bar',
services: {
'45': {
serviceName: 'bazbaz',
imageId: 34,
image: 'foo',
environment: {},
labels: {},
},
},
},
};
expect(validation.isValidAppsObject(apps)).to.equal(true);
});
it('returns false with an invalid environment', () => {
const apps = {
'1234': {
name: 'something',
releaseId: 123,
commit: 'bar',
services: {
'45': {
serviceName: 'bazbaz',
imageId: 34,
image: 'foo',
environment: { ' baz': 'bat' },
labels: {},
},
},
},
};
expect(validation.isValidAppsObject(apps)).to.equal(false);
});
it('returns false with an invalid appId', () => {
const apps = {
boo: {
name: 'something',
releaseId: 123,
commit: 'bar',
services: {
'45': {
serviceName: 'bazbaz',
imageId: 34,
image: 'foo',
environment: {},
labels: {},
},
},
},
};
expect(validation.isValidAppsObject(apps)).to.equal(false);
});
it('returns true with a missing releaseId', () => {
const apps = {
'1234': {
name: 'something',
services: {
'45': {
serviceName: 'bazbaz',
imageId: 34,
image: 'foo',
environment: {},
labels: {},
},
},
},
};
expect(validation.isValidAppsObject(apps)).to.equal(true);
});
it('returns false with an invalid releaseId', () => {
const apps = {
'1234': {
name: 'something',
releaseId: '123a',
services: {
'45': {
serviceName: 'bazbaz',
imageId: 34,
image: 'foo',
environment: {},
labels: {},
},
},
},
};
expect(validation.isValidAppsObject(apps)).to.equal(true);
});
});
describe('isValidDependentDevicesObject', () => {
it('returns true for a valid object', () => {
const devices: Dictionary<any> = {};
devices[almostTooLongText] = {
name: 'foo',
apps: {
'234': {
config: { bar: 'baz' },
environment: { dead: 'beef' },
},
},
};
expect(validation.isValidDependentDevicesObject(devices)).to.equal(true);
});
it('returns false with a missing apps object', () => {
const devices = {
abcd1234: {
name: 'foo',
},
};
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false);
});
it('returns false with an invalid environment', () => {
const devices = {
abcd1234: {
name: 'foo',
apps: {
'234': {
config: { bar: 'baz' },
environment: { dead: 1 },
},
},
},
};
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false);
});
it('returns false if the uuid is too long', () => {
const devices: Dictionary<any> = {};
devices[almostTooLongText + 'a'] = {
name: 'foo',
apps: {
'234': {
config: { bar: 'baz' },
environment: { dead: 'beef' },
},
},
};
return expect(validation.isValidDependentDevicesObject(devices)).to.equal(
false,
);
});
});
});

View File

@ -1,20 +0,0 @@
Promise = require 'bluebird'
constants = require '../src/lib/constants'
fs = Promise.promisifyAll(require('fs'))
blink = require('../src/lib/blink')
{ expect } = require './lib/chai-config'
describe 'blink', ->
it 'is a blink function', ->
expect(blink).to.be.a('function')
it 'has a pattern property with start and stop functions', ->
expect(blink.pattern.start).to.be.a('function')
expect(blink.pattern.stop).to.be.a('function')
it 'writes to a file that represents the LED, and writes a 0 at the end to turn the LED off', ->
blink(1)
.then ->
fs.readFileAsync(constants.ledFile)
.then (contents) ->
expect(contents.toString()).to.equal('0')

22
test/08-blink.spec.ts Normal file
View File

@ -0,0 +1,22 @@
import { fs } from 'mz';
import { expect } from './lib/chai-config';
import blink = require('../src/lib/blink');
import constants = require('../src/lib/constants');
describe('blink', () => {
it('is a blink function', () => expect(blink).to.be.a('function'));
it('has a pattern property with start and stop functions', () => {
expect(blink.pattern.start).to.be.a('function');
expect(blink.pattern.stop).to.be.a('function');
});
it('writes to a file that represents the LED, and writes a 0 at the end to turn the LED off', async () => {
// TODO: Fix the typings for blink
await (blink as any)(1);
const contents = await fs.readFile(constants.ledFile);
expect(contents.toString()).to.equal('0');
});
});

View File

@ -1,128 +0,0 @@
mixpanel = require 'mixpanel'
{ expect } = require './lib/chai-config'
{ stub } = require 'sinon'
supervisorVersion = require '../src/lib/supervisor-version'
{ EventTracker } = require '../src/event-tracker'
describe 'EventTracker', ->
before ->
stub(mixpanel, 'init').callsFake (token) ->
return {
token: token
track: stub().returns()
}
@eventTrackerOffline = new EventTracker()
@eventTracker = new EventTracker()
stub(EventTracker.prototype, 'logEvent')
after ->
EventTracker.prototype.logEvent.restore()
mixpanel.init.restore()
it 'initializes in unmanaged mode', ->
promise = @eventTrackerOffline.init({
unmanaged: true
uuid: 'foobar'
mixpanelHost: { host: '', path: '' }
})
expect(promise).to.be.fulfilled
.then =>
expect(@eventTrackerOffline.client).to.be.null
it 'logs events in unmanaged mode, with the correct properties', ->
@eventTrackerOffline.track('Test event', { appId: 'someValue' })
expect(@eventTrackerOffline.logEvent).to.be.calledWith('Event:', 'Test event', JSON.stringify({ appId: 'someValue' }))
it 'initializes a mixpanel client when not in unmanaged mode', ->
promise = @eventTracker.init({
mixpanelToken: 'someToken'
uuid: 'barbaz'
mixpanelHost: { host: '', path: '' }
})
expect(promise).to.be.fulfilled
.then =>
expect(mixpanel.init).to.have.been.calledWith('someToken')
expect(@eventTracker.client.token).to.equal('someToken')
expect(@eventTracker.client.track).to.be.a('function')
it 'calls the mixpanel client track function with the event, properties and uuid as distinct_id', ->
@eventTracker.track('Test event 2', { appId: 'someOtherValue' })
expect(@eventTracker.logEvent).to.be.calledWith('Event:', 'Test event 2', JSON.stringify({ appId: 'someOtherValue' }))
expect(@eventTracker.client.track).to.be.calledWith('Test event 2', {
appId: 'someOtherValue'
uuid: 'barbaz'
distinct_id: 'barbaz'
supervisorVersion
})
it 'can be passed an Error and it is added to the event properties', ->
theError = new Error('something went wrong')
@eventTracker.track('Error event', theError)
expect(@eventTracker.client.track).to.be.calledWith('Error event', {
error:
message: theError.message
stack: theError.stack
uuid: 'barbaz'
distinct_id: 'barbaz'
supervisorVersion
})
it 'hides service environment variables, to avoid logging keys or secrets', ->
props = {
service:
appId: '1'
environment: {
RESIN_API_KEY: 'foo'
RESIN_SUPERVISOR_API_KEY: 'bar'
OTHER_VAR: 'hi'
}
}
@eventTracker.track('Some app event', props)
expect(@eventTracker.client.track).to.be.calledWith('Some app event', {
service: { appId: '1' }
uuid: 'barbaz'
distinct_id: 'barbaz'
supervisorVersion
})
it 'should handle being passed no properties object', ->
expect(@eventTracker.track('no-options')).to.not.throw
describe 'Rate limiting', ->
it 'should rate limit events of the same type', ->
@eventTracker.client.track.reset()
@eventTracker.track('test', {})
@eventTracker.track('test', {})
@eventTracker.track('test', {})
@eventTracker.track('test', {})
@eventTracker.track('test', {})
expect(@eventTracker.client.track).to.have.callCount(1)
it 'should rate limit events of the same type with different arguments', ->
@eventTracker.client.track.reset()
@eventTracker.track('test2', { a: 1 })
@eventTracker.track('test2', { b: 2 })
@eventTracker.track('test2', { c: 3 })
@eventTracker.track('test2', { d: 4 })
@eventTracker.track('test2', { e: 5 })
expect(@eventTracker.client.track).to.have.callCount(1)
it 'should not rate limit events of different types', ->
@eventTracker.client.track.reset()
@eventTracker.track('test3', { a: 1 })
@eventTracker.track('test4', { b: 2 })
@eventTracker.track('test5', { c: 3 })
@eventTracker.track('test6', { d: 4 })
@eventTracker.track('test7', { e: 5 })
expect(@eventTracker.client.track).to.have.callCount(5)

View File

@ -0,0 +1,168 @@
import * as mixpanel from 'mixpanel';
import { stub } from 'sinon';
import { expect } from './lib/chai-config';
import EventTracker from '../src/event-tracker';
import supervisorVersion = require('../src/lib/supervisor-version');
describe('EventTracker', () => {
let eventTrackerOffline: EventTracker;
let eventTracker: EventTracker;
before(() => {
stub(mixpanel, 'init').callsFake(token => ({
token,
track: stub().returns(undefined),
}));
eventTrackerOffline = new EventTracker();
eventTracker = new EventTracker();
return stub(EventTracker.prototype as any, 'logEvent');
});
after(() => {
(EventTracker.prototype as any).logEvent.restore();
return mixpanel.init.restore();
});
it('initializes in unmanaged mode', () => {
const promise = eventTrackerOffline.init({
unmanaged: true,
uuid: 'foobar',
mixpanelHost: { host: '', path: '' },
mixpanelToken: '',
});
expect(promise).to.be.fulfilled.then(() => {
// @ts-ignore
expect(eventTrackerOffline.client).to.be.null;
});
});
it('logs events in unmanaged mode, with the correct properties', () => {
eventTrackerOffline.track('Test event', { appId: 'someValue' });
// @ts-ignore
expect(eventTrackerOffline.logEvent).to.be.calledWith(
'Event:',
'Test event',
JSON.stringify({ appId: 'someValue' }),
);
});
it('initializes a mixpanel client when not in unmanaged mode', () => {
const promise = eventTracker.init({
mixpanelToken: 'someToken',
uuid: 'barbaz',
mixpanelHost: { host: '', path: '' },
unmanaged: false,
});
expect(promise).to.be.fulfilled.then(() => {
expect(mixpanel.init).to.have.been.calledWith('someToken');
// @ts-ignore
expect(eventTracker.client.token).to.equal('someToken');
// @ts-ignore
expect(eventTracker.client.track).to.be.a('function');
});
});
it('calls the mixpanel client track function with the event, properties and uuid as distinct_id', () => {
eventTracker.track('Test event 2', { appId: 'someOtherValue' });
// @ts-ignore
expect(eventTracker.logEvent).to.be.calledWith(
'Event:',
'Test event 2',
JSON.stringify({ appId: 'someOtherValue' }),
);
// @ts-ignore
expect(eventTracker.client.track).to.be.calledWith('Test event 2', {
appId: 'someOtherValue',
uuid: 'barbaz',
distinct_id: 'barbaz',
supervisorVersion,
});
});
it('can be passed an Error and it is added to the event properties', () => {
const theError = new Error('something went wrong');
eventTracker.track('Error event', theError);
// @ts-ignore
expect(eventTracker.client.track).to.be.calledWith('Error event', {
error: {
message: theError.message,
stack: theError.stack,
},
uuid: 'barbaz',
distinct_id: 'barbaz',
supervisorVersion,
});
});
it('hides service environment variables, to avoid logging keys or secrets', () => {
const props = {
service: {
appId: '1',
environment: {
RESIN_API_KEY: 'foo',
RESIN_SUPERVISOR_API_KEY: 'bar',
OTHER_VAR: 'hi',
},
},
};
eventTracker.track('Some app event', props);
// @ts-ignore
expect(eventTracker.client.track).to.be.calledWith('Some app event', {
service: { appId: '1' },
uuid: 'barbaz',
distinct_id: 'barbaz',
supervisorVersion,
});
});
it('should handle being passed no properties object', () => {
expect(eventTracker.track('no-options')).to.not.throw;
});
return describe('Rate limiting', () => {
it('should rate limit events of the same type', () => {
// @ts-ignore
eventTracker.client.track.reset();
eventTracker.track('test', {});
eventTracker.track('test', {});
eventTracker.track('test', {});
eventTracker.track('test', {});
eventTracker.track('test', {});
// @ts-ignore
expect(eventTracker.client.track).to.have.callCount(1);
});
it('should rate limit events of the same type with different arguments', () => {
// @ts-ignore
eventTracker.client.track.reset();
eventTracker.track('test2', { a: 1 });
eventTracker.track('test2', { b: 2 });
eventTracker.track('test2', { c: 3 });
eventTracker.track('test2', { d: 4 });
eventTracker.track('test2', { e: 5 });
// @ts-ignore
expect(eventTracker.client.track).to.have.callCount(1);
});
it('should not rate limit events of different types', () => {
// @ts-ignore
eventTracker.client.track.reset();
eventTracker.track('test3', { a: 1 });
eventTracker.track('test4', { b: 2 });
eventTracker.track('test5', { c: 3 });
eventTracker.track('test6', { d: 4 });
eventTracker.track('test7', { e: 5 });
// @ts-ignore
expect(eventTracker.client.track).to.have.callCount(5);
});
});
});

View File

@ -1,20 +1,21 @@
os = require 'os'
import * as os from 'os';
import { stub } from 'sinon';
{ expect } = require './lib/chai-config'
{ stub } = require 'sinon'
import { expect } from './lib/chai-config';
network = require '../src/network'
describe 'network', ->
describe 'getIPAddresses', ->
before ->
import * as network from '../src/network';
describe('network', () => {
describe('getIPAddresses', () => {
before(() =>
stub(os, 'networkInterfaces').returns({
lo:
[{
lo: [
{
address: '127.0.0.1',
netmask: '255.0.0.0',
family: 'IPv4',
mac: '00:00:00:00:00:00',
internal: true
internal: true,
},
{
address: '::1',
@ -22,15 +23,16 @@ describe 'network', ->
family: 'IPv6',
mac: '00:00:00:00:00:00',
scopeid: 0,
internal: true
}]
docker0:
[{
internal: true,
},
],
docker0: [
{
address: '172.17.0.1',
netmask: '255.255.0.0',
family: 'IPv4',
mac: '02:42:0f:33:06:ad',
internal: false
internal: false,
},
{
address: 'fe80::42:fff:fe33:6ad',
@ -38,15 +40,16 @@ describe 'network', ->
family: 'IPv6',
mac: '02:42:0f:33:06:ad',
scopeid: 3,
internal: false
}]
wlan0:
[{
internal: false,
},
],
wlan0: [
{
address: '192.168.1.137',
netmask: '255.255.255.0',
family: 'IPv4',
mac: '60:6d:c7:c6:44:3d',
internal: false
internal: false,
},
{
address: '2605:9080:1103:3011:2dbe:35e3:1b5a:b99',
@ -54,18 +57,25 @@ describe 'network', ->
family: 'IPv6',
mac: '60:6d:c7:c6:44:3d',
scopeid: 0,
internal: false
}]
'resin-vpn':
[{
internal: false,
},
],
'resin-vpn': [
{
address: '10.10.2.14',
netmask: '255.255.0.0',
family: 'IPv4',
mac: '01:43:1f:32:05:bd',
internal: false
}]
})
after ->
os.networkInterfaces.restore()
it 'returns only the relevant IP addresses', ->
expect(network.getIPAddresses()).to.deep.equal([ '192.168.1.137' ])
internal: false,
},
],
} as any),
);
// @ts-ignore
after(() => os.networkInterfaces.restore());
it('returns only the relevant IP addresses', () =>
expect(network.getIPAddresses()).to.deep.equal(['192.168.1.137']));
});
});

View File

@ -1,162 +0,0 @@
prepare = require './lib/prepare'
Promise = require 'bluebird'
balenaAPI = require './lib/mocked-balena-api'
fs = Promise.promisifyAll(require('fs'))
{ expect } = require './lib/chai-config'
{ stub, spy } = require 'sinon'
{ DB } = require('../src/db')
{ Config } = require('../src/config')
DeviceState = require('../src/device-state')
{ APIBinder } = require('../src/api-binder')
initModels = (filename) ->
prepare()
@db = new DB()
@config = new Config({ @db, configPath: filename })
@eventTracker = {
track: stub().callsFake (ev, props) ->
console.log(ev, props)
}
@logger = {
clearOutOfDateDBLogs: ->
}
@deviceState = new DeviceState({ @db, @config, @eventTracker, @logger })
@apiBinder = new APIBinder({ @db, @config, @eventTracker, @deviceState })
@db.init()
.then =>
@config.init()
.then =>
@apiBinder.initClient() # Initializes the clients but doesn't trigger provisioning
mockProvisioningOpts = {
apiEndpoint: 'http://0.0.0.0:3000'
uuid: 'abcd'
deviceApiKey: 'averyvalidkey'
provisioningApiKey: 'anotherveryvalidkey'
apiTimeout: 30000
}
describe 'APIBinder', ->
before ->
spy(balenaAPI.balenaBackend, 'registerHandler')
@server = balenaAPI.listen(3000)
after ->
balenaAPI.balenaBackend.registerHandler.restore()
try
@server.close()
# We do not support older OS versions anymore, so we only test this case
describe 'on an OS with deviceApiKey support', ->
before ->
initModels.call(this, '/config-apibinder.json')
it 'provisions a device', ->
promise = @apiBinder.provisionDevice()
expect(promise).to.be.fulfilled
.then =>
expect(balenaAPI.balenaBackend.registerHandler).to.be.calledOnce
balenaAPI.balenaBackend.registerHandler.resetHistory()
expect(@eventTracker.track).to.be.calledWith('Device bootstrap success')
it 'deletes the provisioning key', ->
expect(@config.get('apiKey')).to.eventually.be.undefined
it 'sends the correct parameters when provisioning', ->
fs.readFileAsync('./test/data/config-apibinder.json')
.then(JSON.parse)
.then (conf) ->
expect(balenaAPI.balenaBackend.devices).to.deep.equal({
'1': {
id: 1
user: conf.userId
application: conf.applicationId
uuid: conf.uuid
device_type: conf.deviceType
api_key: conf.deviceApiKey
}
})
describe 'fetchDevice', ->
before ->
initModels.call(this, '/config-apibinder.json')
it 'gets a device by its uuid from the balena API', ->
# Manually add a device to the mocked API
balenaAPI.balenaBackend.devices[3] = {
id: 3
user: 'foo'
application: 1337
uuid: 'abcd'
device_type: 'intel-nuc'
api_key: 'verysecure'
}
@apiBinder.fetchDevice('abcd', 'someApiKey', 30000)
.then (theDevice) ->
expect(theDevice).to.deep.equal(balenaAPI.balenaBackend.devices[3])
describe 'exchangeKeyAndGetDevice', ->
before ->
initModels.call(this, '/config-apibinder.json')
it 'returns the device if it can fetch it with the deviceApiKey', ->
spy(balenaAPI.balenaBackend, 'deviceKeyHandler')
fetchDeviceStub = stub(@apiBinder, 'fetchDevice')
fetchDeviceStub.onCall(0).resolves({ id: 1 })
@apiBinder.exchangeKeyAndGetDevice(mockProvisioningOpts)
.then (device) =>
expect(balenaAPI.balenaBackend.deviceKeyHandler).to.not.be.called
expect(device).to.deep.equal({ id: 1 })
expect(@apiBinder.fetchDevice).to.be.calledOnce
@apiBinder.fetchDevice.restore()
balenaAPI.balenaBackend.deviceKeyHandler.restore()
it 'throws if it cannot get the device with any of the keys', ->
spy(balenaAPI.balenaBackend, 'deviceKeyHandler')
stub(@apiBinder, 'fetchDevice').returns(Promise.resolve(null))
promise = @apiBinder.exchangeKeyAndGetDevice(mockProvisioningOpts)
promise.catch(->)
expect(promise).to.be.rejected
.then =>
expect(balenaAPI.balenaBackend.deviceKeyHandler).to.not.be.called
expect(@apiBinder.fetchDevice).to.be.calledTwice
@apiBinder.fetchDevice.restore()
balenaAPI.balenaBackend.deviceKeyHandler.restore()
it 'exchanges the key and returns the device if the provisioning key is valid', ->
spy(balenaAPI.balenaBackend, 'deviceKeyHandler')
fetchDeviceStub = stub(@apiBinder, 'fetchDevice')
fetchDeviceStub.onCall(0).returns(Promise.resolve(null))
fetchDeviceStub.onCall(1).returns(Promise.resolve({ id: 1 }))
@apiBinder.exchangeKeyAndGetDevice(mockProvisioningOpts)
.then (device) =>
expect(balenaAPI.balenaBackend.deviceKeyHandler).to.be.calledOnce
expect(device).to.deep.equal({ id: 1 })
expect(@apiBinder.fetchDevice).to.be.calledTwice
@apiBinder.fetchDevice.restore()
balenaAPI.balenaBackend.deviceKeyHandler.restore()
describe 'unmanaged mode', ->
before ->
initModels.call(this, '/config-apibinder-offline.json')
it 'does not generate a key if the device is in unmanaged mode', ->
@config.get('unmanaged').then (mode) =>
# Ensure offline mode is set
expect(mode).to.equal(true)
# Check that there is no deviceApiKey
@config.getMany([ 'deviceApiKey', 'uuid' ]).then (conf) ->
expect(conf['deviceApiKey']).to.be.empty
expect(conf['uuid']).to.not.be.undefined
describe 'Minimal config unmanaged mode', ->
before ->
initModels.call(this, '/config-apibinder-offline2.json')
it 'does not generate a key with the minimal config', ->
@config.get('unmanaged').then (mode) =>
expect(mode).to.equal(true)
@config.getMany([ 'deviceApiKey', 'uuid' ]).then (conf) ->
expect(conf['deviceApiKey']).to.be.empty
expect(conf['uuid']).to.not.be.undefined

246
test/11-api-binder.spec.ts Normal file
View File

@ -0,0 +1,246 @@
import { fs } from 'mz';
import { Server } from 'net';
import { spy, stub } from 'sinon';
import chai = require('./lib/chai-config');
import balenaAPI = require('./lib/mocked-balena-api');
import prepare = require('./lib/prepare');
const { expect } = chai;
import ApiBinder from '../src/api-binder';
import Config from '../src/config';
import DB from '../src/db';
import DeviceState = require('../src/device-state');
const initModels = async (obj: Dictionary<any>, filename: string) => {
prepare();
obj.db = new DB();
obj.config = new Config({ db: obj.db, configPath: filename });
obj.eventTracker = {
track: stub().callsFake((ev, props) => console.log(ev, props)),
} as any;
obj.logger = {
clearOutOfDateDBLogs: () => {
/* noop */
},
} as any;
obj.deviceState = new DeviceState({
db: obj.db,
config: obj.config,
eventTracker: obj.eventTracker,
logger: obj.logger,
});
obj.apiBinder = new ApiBinder({
db: obj.db,
config: obj.config,
logger: obj.logger,
eventTracker: obj.eventTracker,
deviceState: obj.deviceState,
});
await obj.db.init();
await obj.config.init();
await obj.apiBinder.initClient(); // Initializes the clients but doesn't trigger provisioning
};
const mockProvisioningOpts = {
apiEndpoint: 'http://0.0.0.0:3000',
uuid: 'abcd',
deviceApiKey: 'averyvalidkey',
provisioningApiKey: 'anotherveryvalidkey',
apiTimeout: 30000,
};
describe('ApiBinder', () => {
let server: Server;
before(() => {
spy(balenaAPI.balenaBackend!, 'registerHandler');
server = balenaAPI.listen(3000);
});
after(() => {
// @ts-ignore
balenaAPI.balenaBackend!.registerHandler.restore();
try {
server.close();
} catch (error) {
/* noop */
}
});
// We do not support older OS versions anymore, so we only test this case
describe('on an OS with deviceApiKey support', () => {
const components: Dictionary<any> = {};
before(() => {
return initModels(components, '/config-apibinder.json');
});
it('provisions a device', () => {
// @ts-ignore
const promise = components.apiBinder.provisionDevice();
return expect(promise).to.be.fulfilled.then(() => {
expect(balenaAPI.balenaBackend!.registerHandler).to.be.calledOnce;
// @ts-ignore
balenaAPI.balenaBackend!.registerHandler.resetHistory();
expect(components.eventTracker.track).to.be.calledWith(
'Device bootstrap success',
);
});
});
it('deletes the provisioning key', async () => {
expect(await components.config.get('apiKey')).to.be.undefined;
});
it('sends the correct parameters when provisioning', async () => {
const conf = JSON.parse(
await fs.readFile('./test/data/config-apibinder.json', 'utf8'),
);
expect(balenaAPI.balenaBackend!.devices).to.deep.equal({
'1': {
id: 1,
user: conf.userId,
application: conf.applicationId,
uuid: conf.uuid,
device_type: conf.deviceType,
api_key: conf.deviceApiKey,
},
});
});
});
describe('fetchDevice', () => {
const components: Dictionary<any> = {};
before(() => {
return initModels(components, '/config-apibinder.json');
});
it('gets a device by its uuid from the balena API', async () => {
// Manually add a device to the mocked API
balenaAPI.balenaBackend!.devices[3] = {
id: 3,
user: 'foo',
application: 1337,
uuid: 'abcd',
device_type: 'intel-nuc',
api_key: 'verysecure',
};
const device = await components.apiBinder.fetchDevice(
'abcd',
'someApiKey',
30000,
);
expect(device).to.deep.equal(balenaAPI.balenaBackend!.devices[3]);
});
});
describe('exchangeKeyAndGetDevice', () => {
const components: Dictionary<any> = {};
before(() => {
return initModels(components, '/config-apibinder.json');
});
it('returns the device if it can fetch it with the deviceApiKey', async () => {
spy(balenaAPI.balenaBackend!, 'deviceKeyHandler');
const fetchDeviceStub = stub(components.apiBinder, 'fetchDevice');
fetchDeviceStub.onCall(0).resolves({ id: 1 });
// @ts-ignore
const device = await components.apiBinder.exchangeKeyAndGetDevice(
mockProvisioningOpts,
);
expect(balenaAPI.balenaBackend!.deviceKeyHandler).to.not.be.called;
expect(device).to.deep.equal({ id: 1 });
expect(components.apiBinder.fetchDevice).to.be.calledOnce;
// @ts-ignore
components.apiBinder.fetchDevice.restore();
// @ts-ignore
balenaAPI.balenaBackend.deviceKeyHandler.restore();
});
it('throws if it cannot get the device with any of the keys', () => {
spy(balenaAPI.balenaBackend!, 'deviceKeyHandler');
stub(components.apiBinder, 'fetchDevice').returns(Promise.resolve(null));
// @ts-ignore
const promise = components.apiBinder.exchangeKeyAndGetDevice(
mockProvisioningOpts,
);
promise.catch(() => {
/* noop */
});
return expect(promise).to.be.rejected.then(() => {
expect(balenaAPI.balenaBackend!.deviceKeyHandler).to.not.be.called;
expect(components.apiBinder.fetchDevice).to.be.calledTwice;
// @ts-ignore
components.apiBinder.fetchDevice.restore();
// @ts-ignore
balenaAPI.balenaBackend.deviceKeyHandler.restore();
});
});
it('exchanges the key and returns the device if the provisioning key is valid', async () => {
spy(balenaAPI.balenaBackend!, 'deviceKeyHandler');
const fetchDeviceStub = stub(components.apiBinder, 'fetchDevice');
fetchDeviceStub.onCall(0).returns(Promise.resolve(null));
fetchDeviceStub.onCall(1).returns(Promise.resolve({ id: 1 }));
// @ts-ignore
const device = await components.apiBinder.exchangeKeyAndGetDevice(
mockProvisioningOpts as any,
);
expect(balenaAPI.balenaBackend!.deviceKeyHandler).to.be.calledOnce;
expect(device).to.deep.equal({ id: 1 });
expect(components.apiBinder.fetchDevice).to.be.calledTwice;
// @ts-ignore
components.apiBinder.fetchDevice.restore();
// @ts-ignore
balenaAPI.balenaBackend.deviceKeyHandler.restore();
});
});
describe('unmanaged mode', () => {
const components: Dictionary<any> = {};
before(() => {
return initModels(components, '/config-apibinder-offline.json');
});
it('does not generate a key if the device is in unmanaged mode', async () => {
const mode = await components.config.get('unmanaged');
// Ensure offline mode is set
expect(mode).to.equal(true);
// Check that there is no deviceApiKey
const conf = await components.config.getMany(['deviceApiKey', 'uuid']);
expect(conf['deviceApiKey']).to.be.empty;
expect(conf['uuid']).to.not.be.undefined;
});
describe('Minimal config unmanaged mode', () => {
const components2: Dictionary<any> = {};
before(() => {
return initModels(components2, '/config-apibinder-offline2.json');
});
it('does not generate a key with the minimal config', async () => {
const mode = await components2.config.get('unmanaged');
expect(mode).to.equal(true);
const conf = await components2.config.getMany(['deviceApiKey', 'uuid']);
expect(conf['deviceApiKey']).to.be.empty;
return expect(conf['uuid']).to.not.be.undefined;
});
});
});
});

View File

@ -1,817 +0,0 @@
exports.targetState = targetState = []
targetState[0] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: {
'23': {
appId: 1234
serviceName: 'aservice'
commit: 'afafafa'
imageId: 12345
image: 'registry2.resin.io/superapp/edfabc:latest'
environment: {
'FOO': 'bar'
}
privileged: false
volumes: []
labels: {}
running: true
},
'24': {
appId: 1234
serviceName: 'anotherService'
commit: 'afafafa'
imageId: 12346
image: 'registry2.resin.io/superapp/afaff:latest'
environment: {
'FOO': 'bro'
}
volumes: []
privileged: false
labels: {}
running: true
}
}
volumes: {}
networks: {}
}
]
}
dependent: { apps: [], devices: [] }
}
targetState[1] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: {
'23': {
appId: 1234
serviceName: 'aservice'
commit: 'afafafa'
imageId: 12345
image: 'registry2.resin.io/superapp/edfabc:latest'
environment: {
'FOO': 'bar'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
volumes: []
labels: {}
running: true
}
}
volumes: {}
networks: {}
}
]
}
dependent: { apps: [], devices: [] }
}
targetState[2] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: {
'23': {
appId: 1234
serviceName: 'aservice'
commit: 'afafafa'
imageId: 12345
image: 'registry2.resin.io/superapp/edfabc:latest'
environment: {
'FOO': 'bar'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
volumes: []
labels: {}
running: true
},
'24': {
appId: 1234
serviceName: 'anotherService'
commit: 'afafafa'
imageId: 12347
image: 'registry2.resin.io/superapp/foooo:latest'
depends_on: [ 'aservice' ]
environment: {
'FOO': 'bro'
'ADDITIONAL_ENV_VAR': 'foo'
}
volumes: []
privileged: false
labels: {}
running: true
}
}
volumes: {}
networks: {}
}
]
}
dependent: { apps: [], devices: [] }
}
targetState[3] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: {
'23': {
appId: 1234
serviceName: 'aservice'
commit: 'afafafa'
imageId: 12345
image: 'registry2.resin.io/superapp/edfabc:latest'
environment: {
'FOO': 'bar'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
volumes: []
labels: {}
running: true
},
'24': {
appId: 1234
serviceName: 'anotherService'
commit: 'afafafa'
imageId: 12347
image: 'registry2.resin.io/superapp/foooo:latest'
environment: {
'FOO': 'bro'
'ADDITIONAL_ENV_VAR': 'foo'
}
volumes: []
privileged: false
labels: {
'io.resin.update.strategy': 'kill-then-download'
}
running: true
}
}
volumes: {}
networks: {}
}
]
}
dependent: { apps: [], devices: [] }
}
targetState[4] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: {
'23': {
appId: 1234
serviceName: 'aservice'
commit: 'afafafa'
imageId: 12345
image: 'registry2.resin.io/superapp/edfabc:latest'
environment: {
'FOO': 'THIS VALUE CHANGED'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
volumes: []
labels: {}
running: true
},
'24': {
appId: 1234
serviceName: 'anotherService'
commit: 'afafafa'
imageId: 12347
image: 'registry2.resin.io/superapp/foooo:latest'
depends_on: [ 'aservice' ]
environment: {
'FOO': 'bro'
'ADDITIONAL_ENV_VAR': 'foo'
}
volumes: []
privileged: false
labels: {}
running: true
}
}
volumes: {}
networks: {}
}
]
}
dependent: { apps: [], devices: [] }
}
targetState[5] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: {
'23': {
appId: 1234
serviceName: 'aservice'
commit: 'afafafa'
imageId: 12345
image: 'registry2.resin.io/superapp/edfabc:latest'
environment: {
'FOO': 'THIS VALUE CHANGED'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
volumes: []
labels: {}
running: true
},
'24': {
appId: 1234
serviceName: 'anotherService'
commit: 'afafafa'
imageId: 12347
image: 'registry2.resin.io/superapp/foooo:latest'
environment: {
'FOO': 'bro'
'ADDITIONAL_ENV_VAR': 'foo'
}
volumes: []
privileged: false
labels: {}
running: true
}
}
volumes: {}
networks: {}
}
]
}
dependent: { apps: [], devices: [] }
}
targetState[6] = {
local: {
name: 'volumeTest'
config: {
}
apps: [
{
appId: 12345
name: 'volumeApp'
commit: 'asd'
releaseId: 3
services: {}
volumes: {}
networks: {}
}
]
}
dependent: { apps: [], devices: [] }
}
exports.currentState = currentState = []
currentState[0] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: [
{
appId: 1234
serviceId: 23
releaseId: 2
commit: 'afafafa'
serviceName: 'aservice'
imageId: 12345
image: 'id1'
environment: {
'FOO': 'bar'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
restart: 'always'
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena'
]
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '23'
'io.resin.supervised': 'true'
'io.resin.service-name': 'aservice'
}
running: true
createdAt: new Date()
containerId: '1'
networkMode: 'default'
networks: { 'default': { aliases: [ 'aservice' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
},
{
appId: 1234
serviceId: 24
releaseId: 2
commit: 'afafafa'
serviceName: 'anotherService'
imageId: 12346
image: 'id0'
environment: {
'FOO': 'bro'
'ADDITIONAL_ENV_VAR': 'foo'
}
volumes: [
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/resin',
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/balena'
]
privileged: false
restart: 'always'
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '24'
'io.resin.supervised': 'true'
'io.resin.service-name': 'anotherService'
}
running: false
createdAt: new Date()
containerId: '2'
networkMode: 'default'
networks: { 'default': { aliases: [ 'anotherService' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
}
]
volumes: {}
networks: { default: {} }
}
]
}
dependent: { apps: [], devices: [] }
}
currentState[1] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: []
volumes: {}
networks: { default: {} }
}
]
}
dependent: { apps: [], devices: [] }
}
currentState[2] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: [
{
appId: 1234
serviceId: 23
releaseId: 2
commit: 'afafafa'
expose: []
ports: []
serviceName: 'aservice'
imageId: 12345
image: 'id1'
environment: {
'FOO': 'THIS VALUE CHANGED'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
restart: 'always'
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena'
]
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '23'
'io.resin.supervised': 'true'
'io.resin.service-name': 'aservice'
}
running: true
createdAt: new Date()
containerId: '1'
networkMode: 'default'
networks: { 'default': { aliases: [ 'aservice' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
}
]
volumes: {}
networks: { default: {} }
}
]
}
dependent: { apps: [], devices: [] }
}
currentState[3] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: [
{
appId: 1234
serviceId: 23
serviceName: 'aservice'
imageId: 12345
releaseId: 2
commit: 'afafafa'
expose: []
ports: []
image: 'id1'
environment: {
'FOO': 'THIS VALUE CHANGED'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
restart: 'always'
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena'
]
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '23'
'io.resin.supervised': 'true'
'io.resin.service-name': 'aservice'
}
running: true
createdAt: new Date(0)
containerId: '1'
networkMode: 'default'
networks: { 'default': { aliases: [ 'aservice' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
},
{
appId: 1234
serviceId: 23
serviceName: 'aservice'
imageId: 12345
releaseId: 2
commit: 'afafafa'
expose: []
ports: []
image: 'id1'
environment: {
'FOO': 'THIS VALUE CHANGED'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
restart: 'always'
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena'
]
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '23'
'io.resin.supervised': 'true'
'io.resin.service-name': 'aservice'
}
running: true
createdAt: new Date(1)
containerId: '2'
networkMode: 'default'
networks: { 'default': { aliases: [ 'aservice' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
}
]
volumes: {}
networks: { default: {} }
}
]
}
dependent: { apps: [], devices: [] }
}
currentState[4] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: [
{
appId: 1234
serviceId: 24
releaseId: 2
commit: 'afafafa'
serviceName: 'anotherService'
imageId: 12346
image: 'id0'
environment: {
'FOO': 'bro'
'ADDITIONAL_ENV_VAR': 'foo'
}
volumes: [
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/resin',
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/balena'
]
privileged: false
restart: 'always'
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '24'
'io.resin.supervised': 'true'
'io.resin.service-name': 'anotherService'
}
running: false
createdAt: new Date()
containerId: '2'
networkMode: 'default'
networks: { 'default': { aliases: [ 'aservice' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
}
]
volumes: {}
networks: { default: {} }
}
]
}
dependent: { apps: [], devices: [] }
}
currentState[5] = {
local: {
name: 'volumeTest'
config: {}
apps: [
{
appId: 12345
name: 'volumeApp'
commit: 'asd'
releaseId: 3
services: []
volumes: {}
networks: { default: {} }
},
{
appId: 12,
name: 'previous-app',
commit: '123',
releaseId: 10
services: [],
networks: {},
volumes: {
my_volume: {}
}
}
]
}
dependent: { apps: [], devices: [] }
}
currentState[6] = {
local: {
name: 'aDeviceWithDifferentName'
config: {
'RESIN_HOST_CONFIG_gpu_mem': '512'
'RESIN_HOST_LOG_TO_DISPLAY': '1'
}
apps: [
{
appId: 1234
name: 'superapp'
commit: 'afafafa'
releaseId: 2
services: [
{
appId: 1234
serviceId: 23
releaseId: 2
commit: 'afafafa'
serviceName: 'aservice'
imageId: 12345
image: 'id1'
environment: {
'FOO': 'bar'
'ADDITIONAL_ENV_VAR': 'foo'
}
privileged: false
restart: 'always'
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena'
]
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '23'
'io.resin.supervised': 'true'
'io.resin.service-name': 'aservice'
}
running: true
createdAt: new Date()
containerId: '1'
networkMode: 'default'
networks: { 'default': { aliases: [ 'aservice' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
},
{
appId: 1234
serviceId: 24
releaseId: 2
commit: 'afafafa'
serviceName: 'anotherService'
imageId: 12346
image: 'id0'
environment: {
'FOO': 'bro'
'ADDITIONAL_ENV_VAR': 'foo'
}
volumes: [
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/resin',
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/balena'
]
privileged: false
restart: 'always'
labels: {
'io.resin.app-id': '1234'
'io.resin.service-id': '24'
'io.resin.supervised': 'true'
'io.resin.service-name': 'anotherService'
}
running: true
createdAt: new Date()
containerId: '2'
networkMode: 'default'
networks: { 'default': { aliases: [ 'anotherService' ] } }
command: [ 'someCommand' ]
entrypoint: [ 'theEntrypoint' ]
}
]
volumes: {}
networks: { default: {} }
}
]
}
dependent: { apps: [], devices: [] }
}
exports.availableImages = availableImages = []
availableImages[0] = [
{
name: 'registry2.resin.io/superapp/afaff:latest'
appId: 1234
serviceId: 24
serviceName: 'anotherService'
imageId: 12346
releaseId: 2
dependent: 0
dockerImageId: 'id0'
},
{
name: 'registry2.resin.io/superapp/edfabc:latest'
appId: 1234
serviceId: 23
serviceName: 'aservice'
imageId: 12345
releaseId: 2
dependent: 0
dockerImageId: 'id1'
}
]
availableImages[1] = [
{
name: 'registry2.resin.io/superapp/foooo:latest'
appId: 1234
serviceId: 24
serviceName: 'anotherService'
imageId: 12347
releaseId: 2
dependent: 0
dockerImageId: 'id2'
},
{
name: 'registry2.resin.io/superapp/edfabc:latest'
appId: 1234
serviceId: 23
serviceName: 'aservice'
imageId: 12345
releaseId: 2
dependent: 0
dockerImageId: 'id1'
}
]
availableImages[2] = [
{
name: 'registry2.resin.io/superapp/foooo:latest'
appId: 1234
serviceId: 24
serviceName: 'anotherService'
imageId: 12347
releaseId: 2
dependent: 0
dockerImageId: 'id2'
}
]

View File

@ -0,0 +1,820 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
export let availableImages: any;
export let currentState: any;
export let targetState: any;
targetState = [];
targetState[0] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: {
'23': {
appId: 1234,
serviceName: 'aservice',
commit: 'afafafa',
imageId: 12345,
image: 'registry2.resin.io/superapp/edfabc:latest',
environment: {
FOO: 'bar',
},
privileged: false,
volumes: [],
labels: {},
running: true,
},
'24': {
appId: 1234,
serviceName: 'anotherService',
commit: 'afafafa',
imageId: 12346,
image: 'registry2.resin.io/superapp/afaff:latest',
environment: {
FOO: 'bro',
},
volumes: [],
privileged: false,
labels: {},
running: true,
},
},
volumes: {},
networks: {},
},
],
},
dependent: { apps: [], devices: [] },
};
targetState[1] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: {
'23': {
appId: 1234,
serviceName: 'aservice',
commit: 'afafafa',
imageId: 12345,
image: 'registry2.resin.io/superapp/edfabc:latest',
environment: {
FOO: 'bar',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
volumes: [],
labels: {},
running: true,
},
},
volumes: {},
networks: {},
},
],
},
dependent: { apps: [], devices: [] },
};
targetState[2] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: {
'23': {
appId: 1234,
serviceName: 'aservice',
commit: 'afafafa',
imageId: 12345,
image: 'registry2.resin.io/superapp/edfabc:latest',
environment: {
FOO: 'bar',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
volumes: [],
labels: {},
running: true,
},
'24': {
appId: 1234,
serviceName: 'anotherService',
commit: 'afafafa',
imageId: 12347,
image: 'registry2.resin.io/superapp/foooo:latest',
depends_on: ['aservice'],
environment: {
FOO: 'bro',
ADDITIONAL_ENV_VAR: 'foo',
},
volumes: [],
privileged: false,
labels: {},
running: true,
},
},
volumes: {},
networks: {},
},
],
},
dependent: { apps: [], devices: [] },
};
targetState[3] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: {
'23': {
appId: 1234,
serviceName: 'aservice',
commit: 'afafafa',
imageId: 12345,
image: 'registry2.resin.io/superapp/edfabc:latest',
environment: {
FOO: 'bar',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
volumes: [],
labels: {},
running: true,
},
'24': {
appId: 1234,
serviceName: 'anotherService',
commit: 'afafafa',
imageId: 12347,
image: 'registry2.resin.io/superapp/foooo:latest',
environment: {
FOO: 'bro',
ADDITIONAL_ENV_VAR: 'foo',
},
volumes: [],
privileged: false,
labels: {
'io.resin.update.strategy': 'kill-then-download',
},
running: true,
},
},
volumes: {},
networks: {},
},
],
},
dependent: { apps: [], devices: [] },
};
targetState[4] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: {
'23': {
appId: 1234,
serviceName: 'aservice',
commit: 'afafafa',
imageId: 12345,
image: 'registry2.resin.io/superapp/edfabc:latest',
environment: {
FOO: 'THIS VALUE CHANGED',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
volumes: [],
labels: {},
running: true,
},
'24': {
appId: 1234,
serviceName: 'anotherService',
commit: 'afafafa',
imageId: 12347,
image: 'registry2.resin.io/superapp/foooo:latest',
depends_on: ['aservice'],
environment: {
FOO: 'bro',
ADDITIONAL_ENV_VAR: 'foo',
},
volumes: [],
privileged: false,
labels: {},
running: true,
},
},
volumes: {},
networks: {},
},
],
},
dependent: { apps: [], devices: [] },
};
targetState[5] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: {
'23': {
appId: 1234,
serviceName: 'aservice',
commit: 'afafafa',
imageId: 12345,
image: 'registry2.resin.io/superapp/edfabc:latest',
environment: {
FOO: 'THIS VALUE CHANGED',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
volumes: [],
labels: {},
running: true,
},
'24': {
appId: 1234,
serviceName: 'anotherService',
commit: 'afafafa',
imageId: 12347,
image: 'registry2.resin.io/superapp/foooo:latest',
environment: {
FOO: 'bro',
ADDITIONAL_ENV_VAR: 'foo',
},
volumes: [],
privileged: false,
labels: {},
running: true,
},
},
volumes: {},
networks: {},
},
],
},
dependent: { apps: [], devices: [] },
};
targetState[6] = {
local: {
name: 'volumeTest',
config: {},
apps: [
{
appId: 12345,
name: 'volumeApp',
commit: 'asd',
releaseId: 3,
services: {},
volumes: {},
networks: {},
},
],
},
dependent: { apps: [], devices: [] },
};
currentState = [];
currentState[0] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: [
{
appId: 1234,
serviceId: 23,
releaseId: 2,
commit: 'afafafa',
serviceName: 'aservice',
imageId: 12345,
image: 'id1',
environment: {
FOO: 'bar',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
restart: 'always',
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena',
],
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '23',
'io.resin.supervised': 'true',
'io.resin.service-name': 'aservice',
},
running: true,
createdAt: new Date(),
containerId: '1',
networkMode: 'default',
networks: { default: { aliases: ['aservice'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
{
appId: 1234,
serviceId: 24,
releaseId: 2,
commit: 'afafafa',
serviceName: 'anotherService',
imageId: 12346,
image: 'id0',
environment: {
FOO: 'bro',
ADDITIONAL_ENV_VAR: 'foo',
},
volumes: [
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/resin',
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/balena',
],
privileged: false,
restart: 'always',
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '24',
'io.resin.supervised': 'true',
'io.resin.service-name': 'anotherService',
},
running: false,
createdAt: new Date(),
containerId: '2',
networkMode: 'default',
networks: { default: { aliases: ['anotherService'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
],
volumes: {},
networks: { default: {} },
},
],
},
dependent: { apps: [], devices: [] },
};
currentState[1] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: [],
volumes: {},
networks: { default: {} },
},
],
},
dependent: { apps: [], devices: [] },
};
currentState[2] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: [
{
appId: 1234,
serviceId: 23,
releaseId: 2,
commit: 'afafafa',
expose: [],
ports: [],
serviceName: 'aservice',
imageId: 12345,
image: 'id1',
environment: {
FOO: 'THIS VALUE CHANGED',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
restart: 'always',
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena',
],
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '23',
'io.resin.supervised': 'true',
'io.resin.service-name': 'aservice',
},
running: true,
createdAt: new Date(),
containerId: '1',
networkMode: 'default',
networks: { default: { aliases: ['aservice'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
],
volumes: {},
networks: { default: {} },
},
],
},
dependent: { apps: [], devices: [] },
};
currentState[3] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: [
{
appId: 1234,
serviceId: 23,
serviceName: 'aservice',
imageId: 12345,
releaseId: 2,
commit: 'afafafa',
expose: [],
ports: [],
image: 'id1',
environment: {
FOO: 'THIS VALUE CHANGED',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
restart: 'always',
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena',
],
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '23',
'io.resin.supervised': 'true',
'io.resin.service-name': 'aservice',
},
running: true,
createdAt: new Date(0),
containerId: '1',
networkMode: 'default',
networks: { default: { aliases: ['aservice'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
{
appId: 1234,
serviceId: 23,
serviceName: 'aservice',
imageId: 12345,
releaseId: 2,
commit: 'afafafa',
expose: [],
ports: [],
image: 'id1',
environment: {
FOO: 'THIS VALUE CHANGED',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
restart: 'always',
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena',
],
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '23',
'io.resin.supervised': 'true',
'io.resin.service-name': 'aservice',
},
running: true,
createdAt: new Date(1),
containerId: '2',
networkMode: 'default',
networks: { default: { aliases: ['aservice'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
],
volumes: {},
networks: { default: {} },
},
],
},
dependent: { apps: [], devices: [] },
};
currentState[4] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: [
{
appId: 1234,
serviceId: 24,
releaseId: 2,
commit: 'afafafa',
serviceName: 'anotherService',
imageId: 12346,
image: 'id0',
environment: {
FOO: 'bro',
ADDITIONAL_ENV_VAR: 'foo',
},
volumes: [
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/resin',
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/balena',
],
privileged: false,
restart: 'always',
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '24',
'io.resin.supervised': 'true',
'io.resin.service-name': 'anotherService',
},
running: false,
createdAt: new Date(),
containerId: '2',
networkMode: 'default',
networks: { default: { aliases: ['aservice'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
],
volumes: {},
networks: { default: {} },
},
],
},
dependent: { apps: [], devices: [] },
};
currentState[5] = {
local: {
name: 'volumeTest',
config: {},
apps: [
{
appId: 12345,
name: 'volumeApp',
commit: 'asd',
releaseId: 3,
services: [],
volumes: {},
networks: { default: {} },
},
{
appId: 12,
name: 'previous-app',
commit: '123',
releaseId: 10,
services: [],
networks: {},
volumes: {
my_volume: {},
},
},
],
},
dependent: { apps: [], devices: [] },
};
currentState[6] = {
local: {
name: 'aDeviceWithDifferentName',
config: {
RESIN_HOST_CONFIG_gpu_mem: '512',
RESIN_HOST_LOG_TO_DISPLAY: '1',
},
apps: [
{
appId: 1234,
name: 'superapp',
commit: 'afafafa',
releaseId: 2,
services: [
{
appId: 1234,
serviceId: 23,
releaseId: 2,
commit: 'afafafa',
serviceName: 'aservice',
imageId: 12345,
image: 'id1',
environment: {
FOO: 'bar',
ADDITIONAL_ENV_VAR: 'foo',
},
privileged: false,
restart: 'always',
volumes: [
'/tmp/balena-supervisor/services/1234/aservice:/tmp/resin',
'/tmp/balena-supervisor/services/1234/aservice:/tmp/balena',
],
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '23',
'io.resin.supervised': 'true',
'io.resin.service-name': 'aservice',
},
running: true,
createdAt: new Date(),
containerId: '1',
networkMode: 'default',
networks: { default: { aliases: ['aservice'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
{
appId: 1234,
serviceId: 24,
releaseId: 2,
commit: 'afafafa',
serviceName: 'anotherService',
imageId: 12346,
image: 'id0',
environment: {
FOO: 'bro',
ADDITIONAL_ENV_VAR: 'foo',
},
volumes: [
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/resin',
'/tmp/balena-supervisor/services/1234/anotherService:/tmp/balena',
],
privileged: false,
restart: 'always',
labels: {
'io.resin.app-id': '1234',
'io.resin.service-id': '24',
'io.resin.supervised': 'true',
'io.resin.service-name': 'anotherService',
},
running: true,
createdAt: new Date(),
containerId: '2',
networkMode: 'default',
networks: { default: { aliases: ['anotherService'] } },
command: ['someCommand'],
entrypoint: ['theEntrypoint'],
},
],
volumes: {},
networks: { default: {} },
},
],
},
dependent: { apps: [], devices: [] },
};
availableImages = [];
availableImages[0] = [
{
name: 'registry2.resin.io/superapp/afaff:latest',
appId: 1234,
serviceId: 24,
serviceName: 'anotherService',
imageId: 12346,
releaseId: 2,
dependent: 0,
dockerImageId: 'id0',
},
{
name: 'registry2.resin.io/superapp/edfabc:latest',
appId: 1234,
serviceId: 23,
serviceName: 'aservice',
imageId: 12345,
releaseId: 2,
dependent: 0,
dockerImageId: 'id1',
},
];
availableImages[1] = [
{
name: 'registry2.resin.io/superapp/foooo:latest',
appId: 1234,
serviceId: 24,
serviceName: 'anotherService',
imageId: 12347,
releaseId: 2,
dependent: 0,
dockerImageId: 'id2',
},
{
name: 'registry2.resin.io/superapp/edfabc:latest',
appId: 1234,
serviceId: 23,
serviceName: 'aservice',
imageId: 12345,
releaseId: 2,
dependent: 0,
dockerImageId: 'id1',
},
];
availableImages[2] = [
{
name: 'registry2.resin.io/superapp/foooo:latest',
appId: 1234,
serviceId: 24,
serviceName: 'anotherService',
imageId: 12347,
releaseId: 2,
dependent: 0,
dockerImageId: 'id2',
},
];

View File

@ -1,8 +0,0 @@
chai = require 'chai'
chaiAsPromised = require('chai-as-promised')
sinonChai = require('sinon-chai')
chai.use(chaiAsPromised)
chai.use(sinonChai)
module.exports = chai

8
test/lib/chai-config.ts Normal file
View File

@ -0,0 +1,8 @@
import * as chai from 'chai';
import chaiAsPromised = require('chai-as-promised');
import sinonChai = require('sinon-chai');
chai.use(chaiAsPromised as any);
chai.use(sinonChai);
export = chai;

View File

@ -1,35 +0,0 @@
express = require 'express'
_ = require 'lodash'
api = express()
api.use(require('body-parser').json())
api.balenaBackend = {
currentId: 1
devices: {}
registerHandler: (req, res) ->
console.log('/device/register called with ', req.body)
device = req.body
device.id = api.balenaBackend.currentId++
api.balenaBackend.devices[device.id] = device
res.status(201).json(device)
getDeviceHandler: (req, res) ->
uuid = req.query['$filter']?.match(/uuid eq '(.*)'/)?[1]
if uuid?
res.json({ d: _.filter(api.balenaBackend.devices, (dev) -> dev.uuid is uuid ) })
else
res.json({ d: [] })
deviceKeyHandler: (req, res) ->
res.status(200).send(req.body.apiKey)
}
api.post '/device/register', (req, res) ->
api.balenaBackend.registerHandler(req, res)
api.get '/v5/device', (req, res) ->
api.balenaBackend.getDeviceHandler(req, res)
api.post '/api-key/device/:deviceId/device-key', (req, res) ->
api.balenaBackend.deviceKeyHandler(req, res)
module.exports = api

View File

@ -0,0 +1,66 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import * as express from 'express';
import * as _ from 'lodash';
const api: express.Express & {
balenaBackend?: {
currentId: number;
devices: { [key: string]: any };
registerHandler: express.RequestHandler;
getDeviceHandler: express.RequestHandler;
deviceKeyHandler: express.RequestHandler;
};
} = express();
// tslint:disable-next-line
api.use(require('body-parser').json());
api.balenaBackend = {
currentId: 1,
devices: {},
registerHandler: (req, res) => {
console.log('/device/register called with ', req.body);
const device = req.body;
device.id = api.balenaBackend!.currentId++;
api.balenaBackend!.devices[device.id] = device;
return res.status(201).json(device);
},
getDeviceHandler: (req, res) => {
const uuid =
req.query['$filter'] != null
? req.query['$filter'].match(/uuid eq '(.*)'/)[1]
: null;
if (uuid != null) {
return res.json({
d: _.filter(api.balenaBackend!.devices, dev => dev.uuid === uuid),
});
} else {
return res.json({ d: [] });
}
},
deviceKeyHandler: (req, res) => {
return res.status(200).send(req.body.apiKey);
},
};
api.post('/device/register', (req, res) =>
api.balenaBackend!.registerHandler(req, res, _.noop),
);
api.get('/v5/device', (req, res) =>
api.balenaBackend!.getDeviceHandler(req, res, _.noop),
);
api.post('/api-key/device/:deviceId/device-key', (req, res) =>
api.balenaBackend!.deviceKeyHandler(req, res, _.noop),
);
export = api;

View File

@ -1,27 +0,0 @@
fs = require('fs')
module.exports = ->
try
fs.unlinkSync(process.env.DATABASE_PATH)
try
fs.unlinkSync(process.env.DATABASE_PATH_2)
try
fs.unlinkSync(process.env.DATABASE_PATH_3)
try
fs.unlinkSync(process.env.LED_FILE)
try
fs.writeFileSync('./test/data/config.json', fs.readFileSync('./test/data/testconfig.json'))
fs.writeFileSync('./test/data/config-apibinder.json', fs.readFileSync('./test/data/testconfig-apibinder.json'))
fs.writeFileSync(
'./test/data/config-apibinder-offline.json',
fs.readFileSync('./test/data/testconfig-apibinder-offline.json')
)
fs.writeFileSync(
'./test/data/config-apibinder-offline2.json',
fs.readFileSync('./test/data/testconfig-apibinder-offline2.json')
)

56
test/lib/prepare.ts Normal file
View File

@ -0,0 +1,56 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import * as fs from 'fs';
export = function() {
try {
fs.unlinkSync(process.env.DATABASE_PATH!);
} catch (e) {
/* ignore /*/
}
try {
fs.unlinkSync(process.env.DATABASE_PATH_2!);
} catch (e) {
/* ignore /*/
}
try {
fs.unlinkSync(process.env.DATABASE_PATH_3!);
} catch (e) {
/* ignore /*/
}
try {
fs.unlinkSync(process.env.LED_FILE!);
} catch (e) {
/* ignore /*/
}
try {
fs.writeFileSync(
'./test/data/config.json',
fs.readFileSync('./test/data/testconfig.json'),
);
fs.writeFileSync(
'./test/data/config-apibinder.json',
fs.readFileSync('./test/data/testconfig-apibinder.json'),
);
fs.writeFileSync(
'./test/data/config-apibinder-offline.json',
fs.readFileSync('./test/data/testconfig-apibinder-offline.json'),
);
return fs.writeFileSync(
'./test/data/config-apibinder-offline2.json',
fs.readFileSync('./test/data/testconfig-apibinder-offline2.json'),
);
} catch (e) {
/* ignore /*/
}
};