mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-10 19:41:42 +00:00
Auto-merge for PR #519 via VersionBot
Add some unit tests to the multicontainer supervisor
This commit is contained in:
commit
06a5e0986c
15
.gitignore
vendored
15
.gitignore
vendored
@ -4,17 +4,14 @@
|
|||||||
/meta-resin/
|
/meta-resin/
|
||||||
*.swp
|
*.swp
|
||||||
/data/
|
/data/
|
||||||
bin/gosuper
|
|
||||||
gosuper/bin/
|
|
||||||
|
|
||||||
base-image/build/bitbake.lock
|
|
||||||
base-image/build/cache
|
|
||||||
base-image/build/downloads
|
|
||||||
base-image/build/sstate-cache
|
|
||||||
base-image/build/tmp-glibc
|
|
||||||
|
|
||||||
/build/
|
/build/
|
||||||
/dist/
|
/dist/
|
||||||
tools/dind/config/
|
tools/dind/config/
|
||||||
tools/dind/config.json*
|
tools/dind/config.json*
|
||||||
tools/dind/apps.json
|
tools/dind/apps.json
|
||||||
|
test/data/config.json
|
||||||
|
test/data/config-apibinder.json
|
||||||
|
test/data/*.sqlite
|
||||||
|
test/data/led_file
|
||||||
|
/coverage/
|
||||||
|
report.xml
|
||||||
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file
|
|||||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## v7.5.2 - 2018-05-01
|
||||||
|
|
||||||
|
* Add some more unit tests to the multicontainer supervisor #519 [Pablo Carranza Velez]
|
||||||
|
|
||||||
## v7.5.1 - 2018-04-29
|
## v7.5.1 - 2018-04-29
|
||||||
|
|
||||||
* Remove trailing slashes from working directories of services #637 [Cameron Diver]
|
* Remove trailing slashes from working directories of services #637 [Cameron Diver]
|
||||||
|
11
package.json
11
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "resin-supervisor",
|
"name": "resin-supervisor",
|
||||||
"description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.",
|
"description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.",
|
||||||
"version": "7.5.1",
|
"version": "7.5.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -11,8 +11,9 @@
|
|||||||
"start": "./entry.sh",
|
"start": "./entry.sh",
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
"lint": "resin-lint src/ test/",
|
"lint": "resin-lint src/ test/",
|
||||||
|
"test": "npm run lint && JUNIT_REPORT_PATH=report.xml mocha --exit -r ts-node/register -r coffee-script/register -r register-coffee-coverage test/*.{js,coffee} && npm run coverage",
|
||||||
"versionist": "versionist",
|
"versionist": "versionist",
|
||||||
"test": "npm run lint && mocha -r ts-node/register -r coffee-script/register test/**/*"
|
"coverage": "istanbul report text && istanbul report html"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash": "^4.14.108",
|
"@types/lodash": "^4.14.108",
|
||||||
@ -28,7 +29,7 @@
|
|||||||
"bluebird": "^3.5.0",
|
"bluebird": "^3.5.0",
|
||||||
"body-parser": "^1.12.0",
|
"body-parser": "^1.12.0",
|
||||||
"buffer-equal-constant-time": "^1.0.1",
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
"chai": "^4.1.2",
|
"chai-events": "0.0.1",
|
||||||
"coffee-loader": "^0.7.3",
|
"coffee-loader": "^0.7.3",
|
||||||
"coffee-script": "~1.11.0",
|
"coffee-script": "~1.11.0",
|
||||||
"copy-webpack-plugin": "^4.2.3",
|
"copy-webpack-plugin": "^4.2.3",
|
||||||
@ -39,6 +40,7 @@
|
|||||||
"duration-js": "^4.0.0",
|
"duration-js": "^4.0.0",
|
||||||
"event-stream": "^3.0.20",
|
"event-stream": "^3.0.20",
|
||||||
"express": "^4.0.0",
|
"express": "^4.0.0",
|
||||||
|
"istanbul": "^0.4.5",
|
||||||
"json-mask": "^0.3.8",
|
"json-mask": "^0.3.8",
|
||||||
"knex": "~0.12.3",
|
"knex": "~0.12.3",
|
||||||
"lockfile": "^1.0.1",
|
"lockfile": "^1.0.1",
|
||||||
@ -47,13 +49,14 @@
|
|||||||
"memoizee": "^0.4.1",
|
"memoizee": "^0.4.1",
|
||||||
"mixpanel": "0.0.20",
|
"mixpanel": "0.0.20",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"mocha": "^5.0.5",
|
"mocha": "^5.1.1",
|
||||||
"mochainon": "^2.0.0",
|
"mochainon": "^2.0.0",
|
||||||
"network-checker": "~0.0.5",
|
"network-checker": "~0.0.5",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"null-loader": "^0.1.1",
|
"null-loader": "^0.1.1",
|
||||||
"pinejs-client": "^2.4.0",
|
"pinejs-client": "^2.4.0",
|
||||||
"pubnub": "^3.7.13",
|
"pubnub": "^3.7.13",
|
||||||
|
"register-coffee-coverage": "0.0.1",
|
||||||
"request": "^2.51.0",
|
"request": "^2.51.0",
|
||||||
"resin-lint": "^1.3.1",
|
"resin-lint": "^1.3.1",
|
||||||
"resin-register-device": "^3.0.0",
|
"resin-register-device": "^3.0.0",
|
||||||
|
@ -165,6 +165,10 @@ module.exports = class Logger
|
|||||||
@opts = opts
|
@opts = opts
|
||||||
@_startBackend()
|
@_startBackend()
|
||||||
|
|
||||||
|
stop: =>
|
||||||
|
if @backend?
|
||||||
|
@backend.stop()
|
||||||
|
|
||||||
_startBackend: =>
|
_startBackend: =>
|
||||||
if checkTruthy(@opts.nativeLogger)
|
if checkTruthy(@opts.nativeLogger)
|
||||||
@backend = new NativeLoggerBackend()
|
@backend = new NativeLoggerBackend()
|
||||||
|
19
test/00-init.coffee
Normal file
19
test/00-init.coffee
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
m = require 'mochainon'
|
||||||
|
|
||||||
|
{ stub } = m.sinon
|
||||||
|
|
||||||
|
dbus = require 'dbus-native'
|
||||||
|
|
||||||
|
stub(dbus, 'systemBus').returns({
|
||||||
|
invoke: (obj, cb) ->
|
||||||
|
console.log(obj)
|
||||||
|
cb()
|
||||||
|
})
|
12
test/01-constants.spec.coffee
Normal file
12
test/01-constants.spec.coffee
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
prepare = require './lib/prepare'
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
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')
|
83
test/02-db.spec.coffee
Normal file
83
test/02-db.spec.coffee
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
prepare = require './lib/prepare'
|
||||||
|
Promise = require 'bluebird'
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
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
|
116
test/03-config.spec.coffee
Normal file
116
test/03-config.spec.coffee
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
prepare = require './lib/prepare'
|
||||||
|
Promise = require 'bluebird'
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
fs = Promise.promisifyAll(require('fs'))
|
||||||
|
m.chai.use(require('chai-events'))
|
||||||
|
|
||||||
|
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.configJsonPath()).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.configJsonPath()).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 'provides the correct pubnub config', ->
|
||||||
|
promise = @conf.get('pubnub')
|
||||||
|
expect(promise).to.eventually.deep.equal({ subscribe_key: 'foo', publish_key: 'bar', ssl: true })
|
||||||
|
|
||||||
|
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('name').then =>
|
||||||
|
@conf.get('name')
|
||||||
|
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
|
@ -1,5 +1,6 @@
|
|||||||
{ expect } = require 'chai'
|
m = require 'mochainon'
|
||||||
Service = require '../../src/compose/service'
|
{ expect } = m.chai
|
||||||
|
Service = require '../src/compose/service'
|
||||||
|
|
||||||
describe 'compose/service.cofee', ->
|
describe 'compose/service.cofee', ->
|
||||||
|
|
277
test/05-device-state.spec.coffee
Normal file
277
test/05-device-state.spec.coffee
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
Promise = require 'bluebird'
|
||||||
|
_ = require 'lodash'
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ stub } = m.sinon
|
||||||
|
m.chai.use(require('chai-events'))
|
||||||
|
{ expect } = m.chai
|
||||||
|
|
||||||
|
prepare = require './lib/prepare'
|
||||||
|
DeviceState = require '../src/device-state'
|
||||||
|
DB = require('../src/db')
|
||||||
|
Config = require('../src/config')
|
||||||
|
|
||||||
|
Service = require '../src/compose/service'
|
||||||
|
|
||||||
|
mockedInitialConfig = {
|
||||||
|
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||||
|
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
|
||||||
|
'RESIN_SUPERVISOR_DELTA': 'false'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': ''
|
||||||
|
'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||||
|
'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: {
|
||||||
|
'RESIN_HOST_CONFIG_gpu_mem': '256'
|
||||||
|
'RESIN_HOST_LOG_TO_DISPLAY': '0'
|
||||||
|
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
|
||||||
|
'RESIN_SUPERVISOR_DELTA': 'false'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': ''
|
||||||
|
'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||||
|
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||||
|
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||||
|
'RESIN_SUPERVISOR_NATIVE_LOGGER': 'true'
|
||||||
|
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||||
|
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||||
|
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
||||||
|
}
|
||||||
|
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'
|
||||||
|
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||||
|
}
|
||||||
|
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: {
|
||||||
|
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||||
|
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||||
|
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
|
||||||
|
'RESIN_SUPERVISOR_DELTA': 'false'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': ''
|
||||||
|
'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||||
|
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||||
|
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||||
|
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||||
|
'RESIN_SUPERVISOR_NATIVE_LOGGER': 'true'
|
||||||
|
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||||
|
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||||
|
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
||||||
|
}
|
||||||
|
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'
|
||||||
|
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||||
|
}
|
||||||
|
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 ->
|
||||||
|
@timeout(5000)
|
||||||
|
prepare()
|
||||||
|
@db = new DB()
|
||||||
|
@config = new Config({ @db })
|
||||||
|
eventTracker = {
|
||||||
|
track: console.log
|
||||||
|
}
|
||||||
|
stub(Service.prototype, 'extendEnvVars').callsFake (env) ->
|
||||||
|
@environment['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||||
|
return @environment
|
||||||
|
@deviceState = new DeviceState({ @db, @config, eventTracker })
|
||||||
|
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
|
||||||
|
@db.init()
|
||||||
|
.then =>
|
||||||
|
@config.init()
|
||||||
|
|
||||||
|
after ->
|
||||||
|
Service.prototype.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 new Service(s)
|
||||||
|
# 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)))
|
||||||
|
@deviceState.applications.images.save.restore()
|
||||||
|
@deviceState.deviceConfig.getCurrent.restore()
|
||||||
|
|
||||||
|
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
|
||||||
|
new Service(s)
|
||||||
|
.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 'applies the target state for device config'
|
||||||
|
|
||||||
|
it 'applies the target state for applications'
|
43
test/06-iptables.spec.coffee
Normal file
43
test/06-iptables.spec.coffee
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
Promise = require 'bluebird'
|
||||||
|
iptables = require '../src/lib/iptables'
|
||||||
|
|
||||||
|
childProcess = require('child_process')
|
||||||
|
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ stub } = m.sinon
|
||||||
|
{ expect } = m.chai
|
||||||
|
|
||||||
|
describe 'iptables', ->
|
||||||
|
it 'calls iptables to delete and recreate rules to block a port', ->
|
||||||
|
stub(childProcess, 'execAsync').returns(Promise.resolve())
|
||||||
|
iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42)
|
||||||
|
.then ->
|
||||||
|
expect(childProcess.execAsync.callCount).to.equal(6)
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j REJECT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j REJECT')
|
||||||
|
.then ->
|
||||||
|
childProcess.execAsync.restore()
|
||||||
|
|
||||||
|
it "falls back to blocking the port with DROP if there's no REJECT support", ->
|
||||||
|
stub(childProcess, 'execAsync').callsFake (cmd) ->
|
||||||
|
if /REJECT$/.test(cmd)
|
||||||
|
Promise.reject()
|
||||||
|
else
|
||||||
|
Promise.resolve()
|
||||||
|
iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42)
|
||||||
|
.then ->
|
||||||
|
expect(childProcess.execAsync.callCount).to.equal(8)
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j REJECT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j REJECT')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j DROP')
|
||||||
|
expect(childProcess.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j DROP')
|
||||||
|
.then ->
|
||||||
|
childProcess.execAsync.restore()
|
@ -2,7 +2,7 @@ _ = require 'lodash'
|
|||||||
m = require 'mochainon'
|
m = require 'mochainon'
|
||||||
{ expect } = m.chai
|
{ expect } = m.chai
|
||||||
|
|
||||||
validation = require '../../src/lib/validation'
|
validation = require '../src/lib/validation'
|
||||||
|
|
||||||
almostTooLongText = _.map([0...255], -> 'a').join('')
|
almostTooLongText = _.map([0...255], -> 'a').join('')
|
||||||
|
|
21
test/08-blink.spec.coffee
Normal file
21
test/08-blink.spec.coffee
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
Promise = require 'bluebird'
|
||||||
|
constants = require '../src/lib/constants'
|
||||||
|
fs = Promise.promisifyAll(require('fs'))
|
||||||
|
blink = require('../src/lib/blink')
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
|
||||||
|
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')
|
79
test/09-event-tracker.spec.coffee
Normal file
79
test/09-event-tracker.spec.coffee
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
mixpanel = require 'mixpanel'
|
||||||
|
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
{ stub } = m.sinon
|
||||||
|
|
||||||
|
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 offline mode', ->
|
||||||
|
promise = @eventTrackerOffline.init({
|
||||||
|
offlineMode: true
|
||||||
|
uuid: 'foobar'
|
||||||
|
})
|
||||||
|
expect(promise).to.be.fulfilled
|
||||||
|
.then =>
|
||||||
|
expect(@eventTrackerOffline._client).to.be.null
|
||||||
|
|
||||||
|
it 'logs events in offline 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 offline mode', ->
|
||||||
|
promise = @eventTracker.init({
|
||||||
|
mixpanelToken: 'someToken'
|
||||||
|
uuid: 'barbaz'
|
||||||
|
})
|
||||||
|
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' })
|
||||||
|
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
|
||||||
|
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'
|
||||||
|
})
|
72
test/10-network.spec.coffee
Normal file
72
test/10-network.spec.coffee
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
os = require 'os'
|
||||||
|
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
{ stub } = m.sinon
|
||||||
|
|
||||||
|
network = require '../src/network'
|
||||||
|
describe 'network', ->
|
||||||
|
describe 'getIPAddresses', ->
|
||||||
|
before ->
|
||||||
|
stub(os, 'networkInterfaces').returns({
|
||||||
|
lo:
|
||||||
|
[{
|
||||||
|
address: '127.0.0.1',
|
||||||
|
netmask: '255.0.0.0',
|
||||||
|
family: 'IPv4',
|
||||||
|
mac: '00:00:00:00:00:00',
|
||||||
|
internal: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: '::1',
|
||||||
|
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
|
||||||
|
family: 'IPv6',
|
||||||
|
mac: '00:00:00:00:00:00',
|
||||||
|
scopeid: 0,
|
||||||
|
internal: true
|
||||||
|
}]
|
||||||
|
docker0:
|
||||||
|
[{
|
||||||
|
address: '172.17.0.1',
|
||||||
|
netmask: '255.255.0.0',
|
||||||
|
family: 'IPv4',
|
||||||
|
mac: '02:42:0f:33:06:ad',
|
||||||
|
internal: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: 'fe80::42:fff:fe33:6ad',
|
||||||
|
netmask: 'ffff:ffff:ffff:ffff::',
|
||||||
|
family: 'IPv6',
|
||||||
|
mac: '02:42:0f:33:06:ad',
|
||||||
|
scopeid: 3,
|
||||||
|
internal: false
|
||||||
|
}]
|
||||||
|
wlan0:
|
||||||
|
[{
|
||||||
|
address: '192.168.1.137',
|
||||||
|
netmask: '255.255.255.0',
|
||||||
|
family: 'IPv4',
|
||||||
|
mac: '60:6d:c7:c6:44:3d',
|
||||||
|
internal: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: '2605:9080:1103:3011:2dbe:35e3:1b5a:b99',
|
||||||
|
netmask: 'ffff:ffff:ffff:ffff::',
|
||||||
|
family: 'IPv6',
|
||||||
|
mac: '60:6d:c7:c6:44:3d',
|
||||||
|
scopeid: 0,
|
||||||
|
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' ])
|
137
test/11-api-binder.spec.coffee
Normal file
137
test/11-api-binder.spec.coffee
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
prepare = require './lib/prepare'
|
||||||
|
Promise = require 'bluebird'
|
||||||
|
resinAPI = require './lib/mocked-resin-api'
|
||||||
|
fs = Promise.promisifyAll(require('fs'))
|
||||||
|
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
{ stub, spy } = m.sinon
|
||||||
|
|
||||||
|
DB = require('../src/db')
|
||||||
|
Config = require('../src/config')
|
||||||
|
DeviceState = require('../src/device-state')
|
||||||
|
APIBinder = require('../src/api-binder')
|
||||||
|
|
||||||
|
initModels = ->
|
||||||
|
@timeout(5000)
|
||||||
|
prepare()
|
||||||
|
@db = new DB()
|
||||||
|
@config = new Config({ @db, configPath: '/config-apibinder.json' })
|
||||||
|
@eventTracker = {
|
||||||
|
track: stub().callsFake (ev, props) ->
|
||||||
|
console.log(ev, props)
|
||||||
|
}
|
||||||
|
@deviceState = new DeviceState({ @db, @config, @eventTracker })
|
||||||
|
@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(resinAPI.resinBackend, 'registerHandler')
|
||||||
|
@server = resinAPI.listen(3000)
|
||||||
|
after ->
|
||||||
|
resinAPI.resinBackend.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)
|
||||||
|
|
||||||
|
it 'provisions a device', ->
|
||||||
|
promise = @apiBinder.provisionDevice()
|
||||||
|
expect(promise).to.be.fulfilled
|
||||||
|
.then =>
|
||||||
|
expect(resinAPI.resinBackend.registerHandler).to.be.calledOnce
|
||||||
|
resinAPI.resinBackend.registerHandler.reset()
|
||||||
|
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(resinAPI.resinBackend.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)
|
||||||
|
|
||||||
|
it 'gets a device by its uuid from the Resin API', ->
|
||||||
|
# Manually add a device to the mocked API
|
||||||
|
resinAPI.resinBackend.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(resinAPI.resinBackend.devices[3])
|
||||||
|
|
||||||
|
describe '_exchangeKeyAndGetDevice', ->
|
||||||
|
before ->
|
||||||
|
initModels.call(this)
|
||||||
|
|
||||||
|
it 'returns the device if it can fetch it with the deviceApiKey', ->
|
||||||
|
spy(resinAPI.resinBackend, 'deviceKeyHandler')
|
||||||
|
fetchDeviceStub = stub(@apiBinder, 'fetchDevice')
|
||||||
|
fetchDeviceStub.onCall(0).resolves({ id: 1 })
|
||||||
|
@apiBinder._exchangeKeyAndGetDevice(mockProvisioningOpts)
|
||||||
|
.then (device) =>
|
||||||
|
expect(resinAPI.resinBackend.deviceKeyHandler).to.not.be.called
|
||||||
|
expect(device).to.deep.equal({ id: 1 })
|
||||||
|
expect(@apiBinder.fetchDevice).to.be.calledOnce
|
||||||
|
@apiBinder.fetchDevice.restore()
|
||||||
|
resinAPI.resinBackend.deviceKeyHandler.restore()
|
||||||
|
|
||||||
|
it 'throws if it cannot get the device with any of the keys', ->
|
||||||
|
spy(resinAPI.resinBackend, 'deviceKeyHandler')
|
||||||
|
stub(@apiBinder, 'fetchDevice').returns(Promise.resolve(null))
|
||||||
|
promise = @apiBinder._exchangeKeyAndGetDevice(mockProvisioningOpts)
|
||||||
|
promise.catch(->)
|
||||||
|
expect(promise).to.be.rejected
|
||||||
|
.then =>
|
||||||
|
expect(resinAPI.resinBackend.deviceKeyHandler).to.not.be.called
|
||||||
|
expect(@apiBinder.fetchDevice).to.be.calledTwice
|
||||||
|
@apiBinder.fetchDevice.restore()
|
||||||
|
resinAPI.resinBackend.deviceKeyHandler.restore()
|
||||||
|
|
||||||
|
it 'exchanges the key and returns the device if the provisioning key is valid', ->
|
||||||
|
spy(resinAPI.resinBackend, '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(resinAPI.resinBackend.deviceKeyHandler).to.be.calledOnce
|
||||||
|
expect(device).to.deep.equal({ id: 1 })
|
||||||
|
expect(@apiBinder.fetchDevice).to.be.calledTwice
|
||||||
|
@apiBinder.fetchDevice.restore()
|
||||||
|
resinAPI.resinBackend.deviceKeyHandler.restore()
|
39
test/12-logger.spec.coffee
Normal file
39
test/12-logger.spec.coffee
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
{ spy, useFakeTimers } = m.sinon
|
||||||
|
|
||||||
|
Logger = require '../src/logger'
|
||||||
|
|
||||||
|
describe 'Logger', ->
|
||||||
|
before ->
|
||||||
|
@fakeBinder = {
|
||||||
|
logBatch: spy()
|
||||||
|
}
|
||||||
|
@fakeEventTracker = {
|
||||||
|
track: spy()
|
||||||
|
}
|
||||||
|
@logger = new Logger({ eventTracker: @fakeEventTracker })
|
||||||
|
@logger.init({ pubnub: {}, channel: 'foo', offlineMode: 'false', enable: 'true', nativeLogger: 'true', apiBinder: @fakeBinder })
|
||||||
|
|
||||||
|
after ->
|
||||||
|
@logger.stop()
|
||||||
|
|
||||||
|
it 'publishes logs to the resin API by default', (done) ->
|
||||||
|
theTime = Date.now()
|
||||||
|
@logger.log(message: 'Hello!', timestamp: theTime)
|
||||||
|
setTimeout( =>
|
||||||
|
expect(@fakeBinder.logBatch).to.be.calledWith([ { message: 'Hello!', timestamp: theTime } ])
|
||||||
|
@fakeBinder.logBatch.reset()
|
||||||
|
done()
|
||||||
|
, 1020)
|
||||||
|
|
||||||
|
it 'allows logging system messages which are also reported to the eventTracker', (done) ->
|
||||||
|
clock = useFakeTimers()
|
||||||
|
clock.tick(10)
|
||||||
|
@logger.logSystemMessage('Hello there!', { someProp: 'someVal' }, 'Some event name')
|
||||||
|
clock.restore()
|
||||||
|
setTimeout( =>
|
||||||
|
expect(@fakeBinder.logBatch).to.be.calledWith([ { message: 'Hello there!', timestamp: 10, isSystem: true } ])
|
||||||
|
expect(@fakeEventTracker.track).to.be.calledWith('Some event name', { someProp: 'someVal' })
|
||||||
|
done()
|
||||||
|
, 1020)
|
152
test/13-device-config.spec.coffee
Normal file
152
test/13-device-config.spec.coffee
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
Promise = require 'bluebird'
|
||||||
|
prepare = require './lib/prepare'
|
||||||
|
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ expect } = m.chai
|
||||||
|
{ stub, spy } = m.sinon
|
||||||
|
|
||||||
|
fsUtils = require '../src/lib/fs-utils'
|
||||||
|
|
||||||
|
DeviceConfig = require '../src/device-config'
|
||||||
|
|
||||||
|
childProcess = require 'child_process'
|
||||||
|
|
||||||
|
describe 'DeviceConfig', ->
|
||||||
|
before ->
|
||||||
|
@timeout(5000)
|
||||||
|
prepare()
|
||||||
|
@fakeDB = {}
|
||||||
|
@fakeConfig = {}
|
||||||
|
@fakeLogger = {
|
||||||
|
logSystemMessage: spy()
|
||||||
|
}
|
||||||
|
@deviceConfig = new DeviceConfig({ logger: @fakeLogger, db: @fakeDB, config: @fakeConfig })
|
||||||
|
|
||||||
|
|
||||||
|
# Test that the format for special values like initramfs and array variables is parsed correctly
|
||||||
|
it 'allows getting boot config with getBootConfig', ->
|
||||||
|
stub(@deviceConfig, 'readBootConfig').resolves('\
|
||||||
|
initramfs initramf.gz 0x00800000\n\
|
||||||
|
dtparam=i2c=on\n\
|
||||||
|
dtparam=audio=on\n\
|
||||||
|
dtoverlay=ads7846\n\
|
||||||
|
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\
|
||||||
|
foobar=baz\n\
|
||||||
|
')
|
||||||
|
@deviceConfig.getBootConfig('raspberry-pi')
|
||||||
|
.then (conf) =>
|
||||||
|
@deviceConfig.readBootConfig.restore()
|
||||||
|
expect(conf).to.deep.equal({
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
|
})
|
||||||
|
|
||||||
|
it 'properly reads a real config.txt file', ->
|
||||||
|
@deviceConfig.getBootConfig('raspberrypi3')
|
||||||
|
.then (conf) ->
|
||||||
|
expect(conf).to.deep.equal({
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_enable_uart: '1'
|
||||||
|
RESIN_HOST_CONFIG_disable_splash: '1'
|
||||||
|
RESIN_HOST_CONFIG_avoid_warnings: '1'
|
||||||
|
RESIN_HOST_CONFIG_gpu_mem: '16'
|
||||||
|
})
|
||||||
|
|
||||||
|
it 'correctly transforms environments to boot config objects', ->
|
||||||
|
bootConfig = @deviceConfig.envToBootConfig({
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
|
})
|
||||||
|
expect(bootConfig).to.deep.equal({
|
||||||
|
initramfs: 'initramf.gz 0x00800000'
|
||||||
|
dtparam: [ 'i2c=on', 'audio=on' ]
|
||||||
|
dtoverlay: [ 'ads7846', 'lirc-rpi,gpio_out_pin=17,gpio_in_pin=13' ]
|
||||||
|
foobar: 'baz'
|
||||||
|
})
|
||||||
|
# Test that the format for special values like initramfs and array variables is preserved
|
||||||
|
it 'does not allow setting forbidden keys', ->
|
||||||
|
current = {
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
|
}
|
||||||
|
target = {
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00810000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
|
}
|
||||||
|
promise = Promise.try =>
|
||||||
|
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||||
|
expect(promise).to.be.rejected
|
||||||
|
promise.catch (err) =>
|
||||||
|
expect(@fakeLogger.logSystemMessage).to.be.calledOnce
|
||||||
|
expect(@fakeLogger.logSystemMessage).to.be.calledWith('Attempt to change blacklisted config value initramfs', {
|
||||||
|
error: 'Attempt to change blacklisted config value initramfs'
|
||||||
|
}, 'Apply boot config error')
|
||||||
|
@fakeLogger.logSystemMessage.reset()
|
||||||
|
|
||||||
|
it 'does not try to change config.txt if it should not change', ->
|
||||||
|
current = {
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
|
}
|
||||||
|
target = {
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
|
}
|
||||||
|
promise = Promise.try =>
|
||||||
|
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||||
|
expect(promise).to.eventually.equal(false)
|
||||||
|
promise.then =>
|
||||||
|
expect(@fakeLogger.logSystemMessage).to.not.be.called
|
||||||
|
@fakeLogger.logSystemMessage.reset()
|
||||||
|
|
||||||
|
it 'writes the target config.txt', ->
|
||||||
|
stub(fsUtils, 'writeFileAtomic').resolves()
|
||||||
|
stub(childProcess, 'execAsync').resolves()
|
||||||
|
current = {
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||||
|
}
|
||||||
|
target = {
|
||||||
|
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||||
|
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=off"'
|
||||||
|
RESIN_HOST_CONFIG_dtoverlay: '"lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||||
|
RESIN_HOST_CONFIG_foobar: 'bat'
|
||||||
|
RESIN_HOST_CONFIG_foobaz: 'bar'
|
||||||
|
}
|
||||||
|
promise = Promise.try =>
|
||||||
|
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||||
|
expect(promise).to.eventually.equal(true)
|
||||||
|
promise.then =>
|
||||||
|
@deviceConfig.setBootConfig('raspberry-pi', target)
|
||||||
|
.then =>
|
||||||
|
expect(childProcess.execAsync).to.be.calledOnce
|
||||||
|
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
||||||
|
expect(@fakeLogger.logSystemMessage.getCall(1).args[2]).to.equal('Apply boot config success')
|
||||||
|
expect(fsUtils.writeFileAtomic).to.be.calledWith('./test/data/mnt/boot/config.txt', '\
|
||||||
|
initramfs initramf.gz 0x00800000\n\
|
||||||
|
dtparam=i2c=on\n\
|
||||||
|
dtparam=audio=off\n\
|
||||||
|
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\
|
||||||
|
foobar=bat\n\
|
||||||
|
foobaz=bar\n\
|
||||||
|
')
|
||||||
|
fsUtils.writeFileAtomic.restore()
|
||||||
|
childProcess.execAsync.restore()
|
||||||
|
@fakeLogger.logSystemMessage.reset()
|
||||||
|
|
||||||
|
# This will require stubbing device.reboot, gosuper.post, config.get/set
|
||||||
|
it 'applies the target state'
|
370
test/14-application-manager.spec.coffee
Normal file
370
test/14-application-manager.spec.coffee
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
Promise = require 'bluebird'
|
||||||
|
_ = require 'lodash'
|
||||||
|
|
||||||
|
m = require 'mochainon'
|
||||||
|
{ stub } = m.sinon
|
||||||
|
m.chai.use(require('chai-events'))
|
||||||
|
{ expect } = m.chai
|
||||||
|
|
||||||
|
prepare = require './lib/prepare'
|
||||||
|
DeviceState = require '../src/device-state'
|
||||||
|
DB = require('../src/db')
|
||||||
|
Config = require('../src/config')
|
||||||
|
Service = require '../src/compose/service'
|
||||||
|
|
||||||
|
{ currentState, targetState, availableImages } = require './lib/application-manager-test-states'
|
||||||
|
|
||||||
|
appDBFormatNormalised = {
|
||||||
|
appId: 1234
|
||||||
|
commit: 'bar'
|
||||||
|
releaseId: 2
|
||||||
|
name: 'app'
|
||||||
|
services: JSON.stringify([
|
||||||
|
{
|
||||||
|
appId: 1234
|
||||||
|
serviceName: 'serv'
|
||||||
|
imageId: 12345
|
||||||
|
environment: { FOO: 'var2' }
|
||||||
|
labels: {}
|
||||||
|
image: 'foo/bar:latest'
|
||||||
|
releaseId: 2
|
||||||
|
serviceId: 4
|
||||||
|
commit: 'bar'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
networks: '{}'
|
||||||
|
volumes: '{}'
|
||||||
|
}
|
||||||
|
|
||||||
|
appStateFormat = {
|
||||||
|
appId: 1234
|
||||||
|
commit: 'bar'
|
||||||
|
releaseId: 2
|
||||||
|
name: 'app'
|
||||||
|
services: {
|
||||||
|
'4': {
|
||||||
|
appId: 1234
|
||||||
|
serviceName: 'serv'
|
||||||
|
imageId: 12345
|
||||||
|
environment: { FOO: 'var2' }
|
||||||
|
labels: {}
|
||||||
|
image: 'foo/bar:latest'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appStateFormatNeedsServiceCreate = {
|
||||||
|
appId: 1234
|
||||||
|
commit: 'bar'
|
||||||
|
releaseId: 2
|
||||||
|
name: 'app'
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
appId: 1234
|
||||||
|
environment: {
|
||||||
|
FOO: 'var2'
|
||||||
|
}
|
||||||
|
imageId: 12345
|
||||||
|
serviceId: 4
|
||||||
|
releaseId: 2
|
||||||
|
serviceName: 'serv'
|
||||||
|
image: 'foo/bar:latest'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
networks: {}
|
||||||
|
volumes: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependentStateFormat = {
|
||||||
|
appId: 1234
|
||||||
|
image: 'foo/bar'
|
||||||
|
commit: 'bar'
|
||||||
|
releaseId: 3
|
||||||
|
name: 'app'
|
||||||
|
config: { RESIN_FOO: 'var' }
|
||||||
|
environment: { FOO: 'var2' }
|
||||||
|
parentApp: 256
|
||||||
|
imageId: 45
|
||||||
|
}
|
||||||
|
|
||||||
|
dependentStateFormatNormalised = {
|
||||||
|
appId: 1234
|
||||||
|
image: 'foo/bar:latest'
|
||||||
|
commit: 'bar'
|
||||||
|
releaseId: 3
|
||||||
|
name: 'app'
|
||||||
|
config: { RESIN_FOO: 'var' }
|
||||||
|
environment: { FOO: 'var2' }
|
||||||
|
parentApp: 256
|
||||||
|
imageId: 45
|
||||||
|
}
|
||||||
|
|
||||||
|
dependentDBFormat = {
|
||||||
|
appId: 1234
|
||||||
|
image: 'foo/bar:latest'
|
||||||
|
commit: 'bar'
|
||||||
|
releaseId: 3
|
||||||
|
name: 'app'
|
||||||
|
config: JSON.stringify({ RESIN_FOO: 'var' })
|
||||||
|
environment: JSON.stringify({ FOO: 'var2' })
|
||||||
|
parentApp: 256
|
||||||
|
imageId: 45
|
||||||
|
}
|
||||||
|
|
||||||
|
describe 'ApplicationManager', ->
|
||||||
|
before ->
|
||||||
|
@timeout(5000)
|
||||||
|
prepare()
|
||||||
|
@db = new DB()
|
||||||
|
@config = new Config({ @db })
|
||||||
|
eventTracker = {
|
||||||
|
track: console.log
|
||||||
|
}
|
||||||
|
@deviceState = new DeviceState({ @db, @config, eventTracker })
|
||||||
|
@applications = @deviceState.applications
|
||||||
|
stub(@applications.images, 'inspectByName').callsFake (imageName) ->
|
||||||
|
Promise.resolve({
|
||||||
|
Config: {
|
||||||
|
Cmd: [ 'someCommand' ]
|
||||||
|
Entrypoint: [ 'theEntrypoint' ]
|
||||||
|
Env: []
|
||||||
|
Labels: {}
|
||||||
|
Volumes: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
stub(@applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1'))
|
||||||
|
stub(Service.prototype, 'extendEnvVars').callsFake (env) ->
|
||||||
|
@environment['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||||
|
return @environment
|
||||||
|
@normaliseCurrent = (current) ->
|
||||||
|
Promise.map current.local.apps, (app) ->
|
||||||
|
Promise.map app.services, (service) ->
|
||||||
|
new Service(service)
|
||||||
|
.then (normalisedServices) ->
|
||||||
|
appCloned = _.clone(app)
|
||||||
|
appCloned.services = normalisedServices
|
||||||
|
return appCloned
|
||||||
|
.then (normalisedApps) ->
|
||||||
|
currentCloned = _.clone(current)
|
||||||
|
currentCloned.local.apps = normalisedApps
|
||||||
|
return currentCloned
|
||||||
|
|
||||||
|
@normaliseTarget = (target, available) =>
|
||||||
|
Promise.map target.local.apps, (app) =>
|
||||||
|
@applications.normaliseAppForDB(app)
|
||||||
|
.then (normalisedApp) =>
|
||||||
|
@applications.normaliseAndExtendAppFromDB(normalisedApp)
|
||||||
|
.then (apps) ->
|
||||||
|
targetCloned = _.cloneDeep(target)
|
||||||
|
# We mock what createTargetService does when an image is available
|
||||||
|
targetCloned.local.apps = _.map apps, (app) ->
|
||||||
|
app.services = _.map app.services, (service) ->
|
||||||
|
img = _.find(available, (i) -> i.name == service.image)
|
||||||
|
if img?
|
||||||
|
service.image = img.dockerImageId
|
||||||
|
return service
|
||||||
|
return app
|
||||||
|
return targetCloned
|
||||||
|
@db.init()
|
||||||
|
.then =>
|
||||||
|
@config.init()
|
||||||
|
|
||||||
|
after ->
|
||||||
|
@applications.images.inspectByName.restore()
|
||||||
|
@applications.docker.getNetworkGateway.restore()
|
||||||
|
Service.prototype.extendEnvVars.restore()
|
||||||
|
|
||||||
|
it 'infers a start step when all that changes is a running state', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[0])
|
||||||
|
@normaliseTarget(targetState[0], availableImages[0])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.deep.equal([{
|
||||||
|
action: 'start'
|
||||||
|
current: current.local.apps[0].services[1]
|
||||||
|
target: target.local.apps[0].services[1]
|
||||||
|
serviceId: 24
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
}])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'infers a kill step when a service has to be removed', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[0])
|
||||||
|
@normaliseTarget(targetState[1], availableImages[0])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.deep.equal([{
|
||||||
|
action: 'kill'
|
||||||
|
current: current.local.apps[0].services[1]
|
||||||
|
target: null
|
||||||
|
serviceId: 24
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
}])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'infers a fetch step when a service has to be updated', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[0])
|
||||||
|
@normaliseTarget(targetState[2], availableImages[0])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.deep.equal([{
|
||||||
|
action: 'fetch'
|
||||||
|
image: @applications.imageForService(target.local.apps[0].services[1])
|
||||||
|
serviceId: 24
|
||||||
|
appId: 1234
|
||||||
|
}])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'does not infer a fetch step when the download is already in progress', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[0])
|
||||||
|
@normaliseTarget(targetState[2], availableImages[0])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[0], [ target.local.apps[0].services[1].imageId ], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.deep.equal([{ action: 'noop', appId: 1234 }])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'infers a kill step when a service has to be updated but the strategy is kill-then-download', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[0])
|
||||||
|
@normaliseTarget(targetState[3], availableImages[0])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.deep.equal([{
|
||||||
|
action: 'kill'
|
||||||
|
current: current.local.apps[0].services[1]
|
||||||
|
target: target.local.apps[0].services[1]
|
||||||
|
serviceId: 24
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
}])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'does not infer to kill a service with default strategy if a dependency is not downloaded', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[4])
|
||||||
|
@normaliseTarget(targetState[4], availableImages[2])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[2], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.deep.equal([{
|
||||||
|
action: 'fetch'
|
||||||
|
image: @applications.imageForService(target.local.apps[0].services[0])
|
||||||
|
serviceId: 23
|
||||||
|
appId: 1234
|
||||||
|
}])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'infers to kill several services as long as there is no unmet dependency', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[0])
|
||||||
|
@normaliseTarget(targetState[5], availableImages[1])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.have.deep.members([
|
||||||
|
{
|
||||||
|
action: 'kill'
|
||||||
|
current: current.local.apps[0].services[0]
|
||||||
|
target: target.local.apps[0].services[0]
|
||||||
|
serviceId: 23
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'kill'
|
||||||
|
current: current.local.apps[0].services[1]
|
||||||
|
target: target.local.apps[0].services[1]
|
||||||
|
serviceId: 24
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'infers to start the dependency first', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[1])
|
||||||
|
@normaliseTarget(targetState[4], availableImages[1])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.have.deep.members([
|
||||||
|
{
|
||||||
|
action: 'start'
|
||||||
|
current: null
|
||||||
|
target: target.local.apps[0].services[0]
|
||||||
|
serviceId: 23
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'infers to start a service once its dependency has been met', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[2])
|
||||||
|
@normaliseTarget(targetState[4], availableImages[1])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.have.deep.members([
|
||||||
|
{
|
||||||
|
action: 'start'
|
||||||
|
current: null
|
||||||
|
target: target.local.apps[0].services[1]
|
||||||
|
serviceId: 24
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'infers to remove spurious containers', ->
|
||||||
|
Promise.join(
|
||||||
|
@normaliseCurrent(currentState[3])
|
||||||
|
@normaliseTarget(targetState[4], availableImages[1])
|
||||||
|
(current, target) =>
|
||||||
|
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||||
|
expect(steps).to.eventually.have.deep.members([
|
||||||
|
{
|
||||||
|
action: 'kill'
|
||||||
|
current: current.local.apps[0].services[0]
|
||||||
|
target: null
|
||||||
|
serviceId: 23
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'start'
|
||||||
|
current: null
|
||||||
|
target: target.local.apps[0].services[1]
|
||||||
|
serviceId: 24
|
||||||
|
appId: 1234
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
it 'converts an app from a state format to a db format, adding missing networks and volumes and normalising the image name', ->
|
||||||
|
app = @applications.normaliseAppForDB(appStateFormat)
|
||||||
|
expect(app).to.eventually.deep.equal(appDBFormatNormalised)
|
||||||
|
|
||||||
|
it 'converts a dependent app from a state format to a db format, normalising the image name', ->
|
||||||
|
app = @applications.proxyvisor.normaliseDependentAppForDB(dependentStateFormat)
|
||||||
|
expect(app).to.eventually.deep.equal(dependentDBFormat)
|
||||||
|
|
||||||
|
it 'converts an app in DB format into state format, adding default and missing fields', ->
|
||||||
|
@applications.normaliseAndExtendAppFromDB(appDBFormatNormalised)
|
||||||
|
.then (app) ->
|
||||||
|
appStateFormatWithDefaults = _.cloneDeep(appStateFormatNeedsServiceCreate)
|
||||||
|
opts = { imageInfo: { Config: { Cmd: [ 'someCommand' ], Entrypoint: [ 'theEntrypoint' ] } } }
|
||||||
|
appStateFormatWithDefaults.services = _.map appStateFormatWithDefaults.services, (service) ->
|
||||||
|
service.imageName = service.image
|
||||||
|
return new Service(service, opts)
|
||||||
|
expect(JSON.parse(JSON.stringify(app))).to.deep.equal(JSON.parse(JSON.stringify(appStateFormatWithDefaults)))
|
||||||
|
|
||||||
|
it 'converts a dependent app in DB format into state format', ->
|
||||||
|
app = @applications.proxyvisor.normaliseDependentAppFromDB(dependentDBFormat)
|
||||||
|
expect(app).to.eventually.deep.equal(dependentStateFormatNormalised)
|
25
test/data/apps.json
Normal file
25
test/data/apps.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "aDevice",
|
||||||
|
"config": {
|
||||||
|
"RESIN_HOST_CONFIG_gpu_mem": "256",
|
||||||
|
"RESIN_HOST_LOG_TO_DISPLAY": "0"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"1234": {
|
||||||
|
"name": "superapp",
|
||||||
|
"commit": "abcdef",
|
||||||
|
"releaseId": 1,
|
||||||
|
"services": {
|
||||||
|
"23": {
|
||||||
|
"imageId": 12345,
|
||||||
|
"serviceName": "someservice",
|
||||||
|
"image": "registry2.resin.io/superapp/abcdef",
|
||||||
|
"labels": {
|
||||||
|
"io.resin.something": "bar"
|
||||||
|
},
|
||||||
|
"environment": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
test/data/etc/hostname
Normal file
1
test/data/etc/hostname
Normal file
@ -0,0 +1 @@
|
|||||||
|
foobardevice
|
2
test/data/etc/os-release
Normal file
2
test/data/etc/os-release
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
PRETTY_NAME="Resin OS 2.0.6 (fake)"
|
||||||
|
VARIANT_ID="dev"
|
2
test/data/etc/os-release-1x
Normal file
2
test/data/etc/os-release-1x
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
PRETTY_NAME="Resin OS 1.27.0 (fake)"
|
||||||
|
VARIANT_ID="dev"
|
1
test/data/etc/os-release-novariant
Normal file
1
test/data/etc/os-release-novariant
Normal file
@ -0,0 +1 @@
|
|||||||
|
PRETTY_NAME="Resin OS 2.0.6 (fake)"
|
1194
test/data/mnt/boot/config.txt
Normal file
1194
test/data/mnt/boot/config.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
test/data/resin-data/.gitkeep
Normal file
1
test/data/resin-data/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
keep me in the repo
|
1
test/data/testconfig-apibinder.json
Normal file
1
test/data/testconfig-apibinder.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"applicationName":"supertestrpi3","applicationId":78373,"deviceType":"raspberrypi3","userId":1001,"username":"someone","appUpdatePollInterval":3000,"listenPort":2345,"vpnPort":443,"apiEndpoint":"http://0.0.0.0:3000","vpnEndpoint":"vpn.resin.io","registryEndpoint":"registry2.resin.io","deltaEndpoint":"https://delta.resin.io","pubnubSubscribeKey":"foo","pubnubPublishKey":"bar","mixpanelToken":"baz","apiKey":"boo","version":"2.0.6+rev3.prod"}
|
1
test/data/testconfig.json
Normal file
1
test/data/testconfig.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"applicationName":"supertestrpi3","applicationId":78373,"deviceType":"raspberrypi3","userId":1001,"username":"someone","appUpdatePollInterval":3000,"listenPort":2345,"vpnPort":443,"apiEndpoint":"https://api.resin.io","vpnEndpoint":"vpn.resin.io","registryEndpoint":"registry2.resin.io","deltaEndpoint":"https://delta.resin.io","pubnubSubscribeKey":"foo","pubnubPublishKey":"bar","mixpanelToken":"baz","apiKey":"boo","version":"2.0.6+rev3.prod"}
|
685
test/lib/application-manager-test-states.coffee
Normal file
685
test/lib/application-manager-test-states.coffee
Normal file
@ -0,0 +1,685 @@
|
|||||||
|
|
||||||
|
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: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
restartPolicy:
|
||||||
|
Name: 'always'
|
||||||
|
MaximumRetryCount: 0
|
||||||
|
volumes: [
|
||||||
|
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||||
|
]
|
||||||
|
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: '1234_default'
|
||||||
|
networks: { '1234_default': {} }
|
||||||
|
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/resin-supervisor/services/1234/anotherService:/tmp/resin'
|
||||||
|
]
|
||||||
|
privileged: false
|
||||||
|
restartPolicy:
|
||||||
|
Name: 'always'
|
||||||
|
MaximumRetryCount: 0
|
||||||
|
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: '1234_default'
|
||||||
|
networks: { '1234_default': {} }
|
||||||
|
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
|
||||||
|
restartPolicy:
|
||||||
|
Name: 'always'
|
||||||
|
MaximumRetryCount: 0
|
||||||
|
volumes: [
|
||||||
|
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||||
|
]
|
||||||
|
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: '1234_default'
|
||||||
|
networks: { '1234_default': {} }
|
||||||
|
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
|
||||||
|
restartPolicy:
|
||||||
|
Name: 'always'
|
||||||
|
MaximumRetryCount: 0
|
||||||
|
volumes: [
|
||||||
|
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||||
|
]
|
||||||
|
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: '1234_default'
|
||||||
|
networks: { '1234_default': {} }
|
||||||
|
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
|
||||||
|
restartPolicy:
|
||||||
|
Name: 'always'
|
||||||
|
MaximumRetryCount: 0
|
||||||
|
volumes: [
|
||||||
|
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||||
|
]
|
||||||
|
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: '1234_default'
|
||||||
|
networks: { '1234_default': {} }
|
||||||
|
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/resin-supervisor/services/1234/anotherService:/tmp/resin'
|
||||||
|
]
|
||||||
|
privileged: false
|
||||||
|
restartPolicy:
|
||||||
|
Name: 'always'
|
||||||
|
MaximumRetryCount: 0
|
||||||
|
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: '1234_default'
|
||||||
|
networks: { '1234_default': {} }
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
]
|
35
test/lib/mocked-resin-api.coffee
Normal file
35
test/lib/mocked-resin-api.coffee
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
express = require 'express'
|
||||||
|
_ = require 'lodash'
|
||||||
|
api = express()
|
||||||
|
api.use(require('body-parser').json())
|
||||||
|
|
||||||
|
api.resinBackend = {
|
||||||
|
currentId: 1
|
||||||
|
devices: {}
|
||||||
|
registerHandler: (req, res) ->
|
||||||
|
console.log('/device/register called with ', req.body)
|
||||||
|
device = req.body
|
||||||
|
device.id = api.resinBackend.currentId++
|
||||||
|
api.resinBackend.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.resinBackend.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.resinBackend.registerHandler(req, res)
|
||||||
|
|
||||||
|
api.get '/v4/device', (req, res) ->
|
||||||
|
api.resinBackend.getDeviceHandler(req, res)
|
||||||
|
|
||||||
|
api.post '/api-key/device/:deviceId/device-key', (req, res) ->
|
||||||
|
api.resinBackend.deviceKeyHandler(req, res)
|
||||||
|
|
||||||
|
module.exports = api
|
18
test/lib/prepare.coffee
Normal file
18
test/lib/prepare.coffee
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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'))
|
Loading…
x
Reference in New Issue
Block a user