mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-25 08:21:07 +00:00
commit
065f79390f
@ -4,6 +4,13 @@ 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.16.0 - 2018-07-23
|
||||||
|
|
||||||
|
* Logger: Add unit tests #701 [Petros Angelatos]
|
||||||
|
* Logger: Remove pubnub leftovers #701 [Petros Angelatos]
|
||||||
|
* Logger: Only send logs produced after attaching #701 [Petros Angelatos]
|
||||||
|
* Logger: Use the new logging backend #701 [Petros Angelatos]
|
||||||
|
|
||||||
## v7.15.0 - 2018-07-17
|
## v7.15.0 - 2018-07-17
|
||||||
|
|
||||||
* Allow the enabling and disabling of persistent logging via env var #700 [Cameron Diver]
|
* Allow the enabling and disabling of persistent logging via env var #700 [Cameron Diver]
|
||||||
|
@ -101,8 +101,6 @@ RUN [ "cross-build-end" ]
|
|||||||
FROM resin/$ARCH-supervisor-base:v1.2.0
|
FROM resin/$ARCH-supervisor-base:v1.2.0
|
||||||
ARG ARCH
|
ARG ARCH
|
||||||
ARG VERSION=master
|
ARG VERSION=master
|
||||||
ARG DEFAULT_PUBNUB_PUBLISH_KEY=pub-c-bananas
|
|
||||||
ARG DEFAULT_PUBNUB_SUBSCRIBE_KEY=sub-c-bananas
|
|
||||||
ARG DEFAULT_MIXPANEL_TOKEN=bananasbananas
|
ARG DEFAULT_MIXPANEL_TOKEN=bananasbananas
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@ -117,8 +115,6 @@ ENV CONFIG_MOUNT_POINT=/boot/config.json \
|
|||||||
LED_FILE=/dev/null \
|
LED_FILE=/dev/null \
|
||||||
SUPERVISOR_IMAGE=resin/$ARCH-supervisor \
|
SUPERVISOR_IMAGE=resin/$ARCH-supervisor \
|
||||||
VERSION=$VERSION \
|
VERSION=$VERSION \
|
||||||
DEFAULT_PUBNUB_PUBLISH_KEY=$DEFAULT_PUBNUB_PUBLISH_KEY \
|
|
||||||
DEFAULT_PUBNUB_SUBSCRIBE_KEY=$DEFAULT_PUBNUB_SUBSCRIBE_KEY \
|
|
||||||
DEFAULT_MIXPANEL_TOKEN=$DEFAULT_MIXPANEL_TOKEN
|
DEFAULT_MIXPANEL_TOKEN=$DEFAULT_MIXPANEL_TOKEN
|
||||||
|
|
||||||
HEALTHCHECK --interval=5m --start-period=1m --timeout=30s --retries=3 \
|
HEALTHCHECK --interval=5m --start-period=1m --timeout=30s --retries=3 \
|
||||||
|
8
Makefile
8
Makefile
@ -12,7 +12,7 @@
|
|||||||
# Variables for build targets:
|
# Variables for build targets:
|
||||||
# * ARCH: amd64/rpi/i386/armv7hf/armel/aarch64 architecture for which to build the supervisor - default: amd64
|
# * ARCH: amd64/rpi/i386/armv7hf/armel/aarch64 architecture for which to build the supervisor - default: amd64
|
||||||
# * IMAGE: image to build or deploy - default: resin/$(ARCH)-supervisor:latest
|
# * IMAGE: image to build or deploy - default: resin/$(ARCH)-supervisor:latest
|
||||||
# * MIXPANEL_TOKEN, PUBNUB_SUBSCRIBE_KEY, PUBNUB_PUBLISH_KEY: (optional) default pubnub and mixpanel keys to embed in the supervisor image
|
# * MIXPANEL_TOKEN: (optional) default mixpanel key to embed in the supervisor image
|
||||||
# * DISABLE_CACHE: if set to true, run build with no cache - default: false
|
# * DISABLE_CACHE: if set to true, run build with no cache - default: false
|
||||||
# * DOCKER_BUILD_OPTIONS: Additional options for docker build, like --cache-from parameters
|
# * DOCKER_BUILD_OPTIONS: Additional options for docker build, like --cache-from parameters
|
||||||
#
|
#
|
||||||
@ -69,9 +69,7 @@ DOCKER_MAJOR_VERSION:=$(word 1, $(subst ., ,$(DOCKER_VERSION)))
|
|||||||
DOCKER_MINOR_VERSION:=$(word 2, $(subst ., ,$(DOCKER_VERSION)))
|
DOCKER_MINOR_VERSION:=$(word 2, $(subst ., ,$(DOCKER_VERSION)))
|
||||||
DOCKER_GE_17_05 := $(shell [ $(DOCKER_MAJOR_VERSION) -gt 17 -o \( $(DOCKER_MAJOR_VERSION) -eq 17 -a $(DOCKER_MINOR_VERSION) -ge 5 \) ] && echo true)
|
DOCKER_GE_17_05 := $(shell [ $(DOCKER_MAJOR_VERSION) -gt 17 -o \( $(DOCKER_MAJOR_VERSION) -eq 17 -a $(DOCKER_MINOR_VERSION) -ge 5 \) ] && echo true)
|
||||||
|
|
||||||
# Default values for Pubnub and Mixpanel keys
|
# Default values for Mixpanel key
|
||||||
PUBNUB_SUBSCRIBE_KEY ?= sub-c-bananas
|
|
||||||
PUBNUB_PUBLISH_KEY ?= pub-c-bananas
|
|
||||||
MIXPANEL_TOKEN ?= bananasbananas
|
MIXPANEL_TOKEN ?= bananasbananas
|
||||||
|
|
||||||
# Default architecture and output image
|
# Default architecture and output image
|
||||||
@ -151,8 +149,6 @@ endif
|
|||||||
--no-cache=$(DISABLE_CACHE) \
|
--no-cache=$(DISABLE_CACHE) \
|
||||||
--build-arg ARCH=$(ARCH) \
|
--build-arg ARCH=$(ARCH) \
|
||||||
--build-arg VERSION=$(shell jq -r .version package.json) \
|
--build-arg VERSION=$(shell jq -r .version package.json) \
|
||||||
--build-arg DEFAULT_PUBNUB_PUBLISH_KEY=$(PUBNUB_PUBLISH_KEY) \
|
|
||||||
--build-arg DEFAULT_PUBNUB_SUBSCRIBE_KEY=$(PUBNUB_SUBSCRIBE_KEY) \
|
|
||||||
--build-arg DEFAULT_MIXPANEL_TOKEN=$(MIXPANEL_TOKEN) \
|
--build-arg DEFAULT_MIXPANEL_TOKEN=$(MIXPANEL_TOKEN) \
|
||||||
-t $(IMAGE) .
|
-t $(IMAGE) .
|
||||||
|
|
||||||
|
@ -32,8 +32,6 @@ The `config.json` file should look something like this:
|
|||||||
"apiEndpoint": "https://api.resinstaging.io", /* Endpoint for the resin.io API */
|
"apiEndpoint": "https://api.resinstaging.io", /* Endpoint for the resin.io API */
|
||||||
"deltaEndpoint": "https://delta.resinstaging.io", /* Endpoint for the delta server to download Docker binary diffs */
|
"deltaEndpoint": "https://delta.resinstaging.io", /* Endpoint for the delta server to download Docker binary diffs */
|
||||||
"vpnEndpoint": "vpn.resinstaging.io", /* Endpoint for the resin.io VPN server */
|
"vpnEndpoint": "vpn.resinstaging.io", /* Endpoint for the resin.io VPN server */
|
||||||
"pubnubSubscribeKey": "sub-c-aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", /* Subscribe key for Pubnub for logs */
|
|
||||||
"pubnubPublishKey": "pub-c-aaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", /* Publish key for Pubnub for logs */
|
|
||||||
"listenPort": 48484, /* Listen port for the supervisor API */
|
"listenPort": 48484, /* Listen port for the supervisor API */
|
||||||
"mixpanelToken": "aaaaaaaaaaaaaaaaaaaaaaaaaa", /* Mixpanel token to report events */
|
"mixpanelToken": "aaaaaaaaaaaaaaaaaaaaaaaaaa", /* Mixpanel token to report events */
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
# * PUSH_IMAGES
|
# * PUSH_IMAGES
|
||||||
# * CLEANUP
|
# * CLEANUP
|
||||||
# * ENABLE_TESTS
|
# * ENABLE_TESTS
|
||||||
# * PUBNUB_SUBSCRIBE_KEY, PUBNUB_PUBLISH_KEY, MIXPANEL_TOKEN: default keys to inject in the supervisor image
|
# * MIXPANEL_TOKEN: default key to inject in the supervisor image
|
||||||
# * EXTRA_TAG: when PUSH_IMAGES is true, additional tag to push to the registries
|
# * EXTRA_TAG: when PUSH_IMAGES is true, additional tag to push to the registries
|
||||||
#
|
#
|
||||||
# Builds the supervisor for the architecture defined by $ARCH.
|
# Builds the supervisor for the architecture defined by $ARCH.
|
||||||
@ -76,8 +76,6 @@ tryPullForCache $NODE_BUILD_CACHE_MASTER
|
|||||||
|
|
||||||
export DOCKER_BUILD_OPTIONS=${CACHE_FROM}
|
export DOCKER_BUILD_OPTIONS=${CACHE_FROM}
|
||||||
export ARCH
|
export ARCH
|
||||||
export PUBNUB_PUBLISH_KEY
|
|
||||||
export PUBNUB_SUBSCRIBE_KEY
|
|
||||||
export MIXPANEL_TOKEN
|
export MIXPANEL_TOKEN
|
||||||
|
|
||||||
make IMAGE=$NODE_BUILD_IMAGE nodebuild
|
make IMAGE=$NODE_BUILD_IMAGE nodebuild
|
||||||
|
@ -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.15.0",
|
"version": "7.16.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -62,7 +62,6 @@
|
|||||||
"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",
|
|
||||||
"register-coffee-coverage": "0.0.1",
|
"register-coffee-coverage": "0.0.1",
|
||||||
"request": "^2.51.0",
|
"request": "^2.51.0",
|
||||||
"resin-lint": "^1.5.7",
|
"resin-lint": "^1.5.7",
|
||||||
|
@ -252,7 +252,6 @@ module.exports = class APIBinder
|
|||||||
belongs_to__user: conf.userId
|
belongs_to__user: conf.userId
|
||||||
is_managed_by__device: conf.deviceId
|
is_managed_by__device: conf.deviceId
|
||||||
uuid: deviceRegister.generateUniqueKey()
|
uuid: deviceRegister.generateUniqueKey()
|
||||||
logs_channel: deviceRegister.generateUniqueKey()
|
|
||||||
registered_at: Math.floor(Date.now() / 1000)
|
registered_at: Math.floor(Date.now() / 1000)
|
||||||
})
|
})
|
||||||
@resinApi.post
|
@resinApi.post
|
||||||
@ -306,27 +305,6 @@ module.exports = class APIBinder
|
|||||||
console.log('Could not pin device to release!')
|
console.log('Could not pin device to release!')
|
||||||
console.log('Error: ', e)
|
console.log('Error: ', e)
|
||||||
|
|
||||||
_sendLogsRequest: (uuid, data) =>
|
|
||||||
reqBody = _.map(data, (msg) -> _.mapKeys(msg, (v, k) -> _.snakeCase(k)))
|
|
||||||
@config.get('apiEndpoint')
|
|
||||||
.then (resinApiEndpoint) =>
|
|
||||||
endpoint = url.resolve(resinApiEndpoint, "/device/v2/#{uuid}/logs")
|
|
||||||
requestParams = _.extend
|
|
||||||
method: 'POST'
|
|
||||||
url: endpoint
|
|
||||||
body: reqBody
|
|
||||||
, @cachedResinApi.passthrough
|
|
||||||
|
|
||||||
@cachedResinApi._request(requestParams)
|
|
||||||
|
|
||||||
logDependent: (uuid, msg) =>
|
|
||||||
@_sendLogsRequest(uuid, [ msg ])
|
|
||||||
|
|
||||||
logBatch: (messages) =>
|
|
||||||
@config.get('uuid')
|
|
||||||
.then (uuid) =>
|
|
||||||
@_sendLogsRequest(uuid, messages)
|
|
||||||
|
|
||||||
# Creates the necessary config vars in the API to match the current device state,
|
# Creates the necessary config vars in the API to match the current device state,
|
||||||
# without overwriting any variables that are already set.
|
# without overwriting any variables that are already set.
|
||||||
_reportInitialEnv: (apiEndpoint) =>
|
_reportInitialEnv: (apiEndpoint) =>
|
||||||
|
@ -38,8 +38,6 @@ class Config extends EventEmitter {
|
|||||||
registered_at: { source: 'config.json', mutable: true },
|
registered_at: { source: 'config.json', mutable: true },
|
||||||
applicationId: { source: 'config.json' },
|
applicationId: { source: 'config.json' },
|
||||||
appUpdatePollInterval: { source: 'config.json', mutable: true, default: 60000 },
|
appUpdatePollInterval: { source: 'config.json', mutable: true, default: 60000 },
|
||||||
pubnubSubscribeKey: { source: 'config.json', default: constants.defaultPubnubSubscribeKey },
|
|
||||||
pubnubPublishKey: { source: 'config.json', default: constants.defaultPubnubPublishKey },
|
|
||||||
mixpanelToken: { source: 'config.json', default: constants.defaultMixpanelToken },
|
mixpanelToken: { source: 'config.json', default: constants.defaultMixpanelToken },
|
||||||
bootstrapRetryDelay: { source: 'config.json', default: 30000 },
|
bootstrapRetryDelay: { source: 'config.json', default: 30000 },
|
||||||
supervisorOfflineMode: { source: 'config.json', default: false },
|
supervisorOfflineMode: { source: 'config.json', default: false },
|
||||||
@ -49,7 +47,6 @@ class Config extends EventEmitter {
|
|||||||
version: { source: 'func' },
|
version: { source: 'func' },
|
||||||
currentApiKey: { source: 'func' },
|
currentApiKey: { source: 'func' },
|
||||||
offlineMode: { source: 'func' },
|
offlineMode: { source: 'func' },
|
||||||
pubnub: { source: 'func' },
|
|
||||||
provisioned: { source: 'func' },
|
provisioned: { source: 'func' },
|
||||||
osVersion: { source: 'func' },
|
osVersion: { source: 'func' },
|
||||||
osVariant: { source: 'func' },
|
osVariant: { source: 'func' },
|
||||||
@ -75,20 +72,16 @@ class Config extends EventEmitter {
|
|||||||
deltaVersion: { source: 'db', mutable: true, default: '2' },
|
deltaVersion: { source: 'db', mutable: true, default: '2' },
|
||||||
lockOverride: { source: 'db', mutable: true, default: 'false' },
|
lockOverride: { source: 'db', mutable: true, default: 'false' },
|
||||||
legacyAppsPresent: { source: 'db', mutable: true, default: 'false' },
|
legacyAppsPresent: { source: 'db', mutable: true, default: 'false' },
|
||||||
nativeLogger: { source: 'db', mutable: true, default: 'true' },
|
|
||||||
// a JSON value, which is either null, or { app: number, commit: string }
|
// a JSON value, which is either null, or { app: number, commit: string }
|
||||||
pinDevice: { source: 'db', mutable: true, default: 'null' },
|
pinDevice: { source: 'db', mutable: true, default: 'null' },
|
||||||
currentCommit: { source: 'db', mutable: true },
|
currentCommit: { source: 'db', mutable: true },
|
||||||
|
|
||||||
// Mutable functions, defined in mutableFuncs
|
|
||||||
logsChannelSecret: { source: 'func', mutable: true },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor({ db, configPath }: ConfigOpts) {
|
public constructor({ db, configPath }: ConfigOpts) {
|
||||||
super();
|
super();
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.configJsonBackend = new ConfigJsonConfigBackend(this.schema, configPath);
|
this.configJsonBackend = new ConfigJsonConfigBackend(this.schema, configPath);
|
||||||
this.providerFunctions = createProviderFunctions(this, db);
|
this.providerFunctions = createProviderFunctions(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(): Bluebird<void> {
|
public init(): Bluebird<void> {
|
||||||
@ -245,10 +238,9 @@ class Config extends EventEmitter {
|
|||||||
'uuid',
|
'uuid',
|
||||||
'deviceApiKey',
|
'deviceApiKey',
|
||||||
'apiSecret',
|
'apiSecret',
|
||||||
'logsChannelSecret',
|
|
||||||
'offlineMode',
|
'offlineMode',
|
||||||
])
|
])
|
||||||
.then(({ uuid, deviceApiKey, apiSecret, logsChannelSecret, offlineMode }) => {
|
.then(({ uuid, deviceApiKey, apiSecret, offlineMode }) => {
|
||||||
// These fields need to be set regardless
|
// These fields need to be set regardless
|
||||||
if (uuid == null || apiSecret == null) {
|
if (uuid == null || apiSecret == null) {
|
||||||
uuid = uuid || this.newUniqueKey();
|
uuid = uuid || this.newUniqueKey();
|
||||||
@ -259,10 +251,8 @@ class Config extends EventEmitter {
|
|||||||
if (offlineMode) {
|
if (offlineMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (deviceApiKey == null || logsChannelSecret == null) {
|
if (deviceApiKey == null) {
|
||||||
deviceApiKey = deviceApiKey || this.newUniqueKey();
|
return this.set({ deviceApiKey: this.newUniqueKey() });
|
||||||
logsChannelSecret = logsChannelSecret || this.newUniqueKey();
|
|
||||||
return this.set({ deviceApiKey, logsChannelSecret });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,6 @@ import { Transaction } from 'knex';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import Config = require('../config');
|
import Config = require('../config');
|
||||||
import DB = require('../db');
|
|
||||||
import supervisorVersion = require('../lib/supervisor-version');
|
import supervisorVersion = require('../lib/supervisor-version');
|
||||||
|
|
||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
@ -25,41 +24,8 @@ export interface ConfigProviderFunctions {
|
|||||||
[key: string]: ConfigProviderFunction;
|
[key: string]: ConfigProviderFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProviderFunctions(config: Config, db: DB): ConfigProviderFunctions {
|
export function createProviderFunctions(config: Config): ConfigProviderFunctions {
|
||||||
return {
|
return {
|
||||||
logsChannelSecret: {
|
|
||||||
get: () => {
|
|
||||||
// Return the logsChannelSecret which corresponds to the current backend
|
|
||||||
return config.get('apiEndpoint')
|
|
||||||
.then((backend = '') => {
|
|
||||||
return db.models('logsChannelSecret').select('secret').where({ backend });
|
|
||||||
})
|
|
||||||
.then(([ conf ]) => {
|
|
||||||
if (conf != null) {
|
|
||||||
return conf.secret;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
set: (value: string, tx?: Transaction) => {
|
|
||||||
// Store the secret with the current backend
|
|
||||||
return config.get('apiEndpoint')
|
|
||||||
.then((backend: string) => {
|
|
||||||
return db.upsertModel(
|
|
||||||
'logsChannelSecret',
|
|
||||||
{ backend: backend || '', secret: value },
|
|
||||||
{ backend: backend || '' },
|
|
||||||
tx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
remove: () => {
|
|
||||||
return config.get('apiEndpoint')
|
|
||||||
.then((backend) => {
|
|
||||||
return db.models('logsChannelSecret').where({ backend: backend || '' }).del();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
version: {
|
version: {
|
||||||
get: () => {
|
get: () => {
|
||||||
return Bluebird.resolve(supervisorVersion);
|
return Bluebird.resolve(supervisorVersion);
|
||||||
@ -81,18 +47,6 @@ export function createProviderFunctions(config: Config, db: DB): ConfigProviderF
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pubnub: {
|
|
||||||
get: () => {
|
|
||||||
return config.getMany([ 'pubnubSubscribeKey', 'pubnubPublishKey' ])
|
|
||||||
.then(({ pubnubSubscribeKey, pubnubPublishKey }) => {
|
|
||||||
return {
|
|
||||||
subscribe_key: pubnubSubscribeKey,
|
|
||||||
publish_key: pubnubPublishKey,
|
|
||||||
ssl: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
provisioned: {
|
provisioned: {
|
||||||
get: () => {
|
get: () => {
|
||||||
return config.getMany([
|
return config.getMany([
|
||||||
|
@ -23,7 +23,6 @@ module.exports = class DeviceConfig
|
|||||||
deltaRetryInterval: { envVarName: 'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL', varType: 'int', defaultValue: '10000' }
|
deltaRetryInterval: { envVarName: 'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL', varType: 'int', defaultValue: '10000' }
|
||||||
deltaVersion: { envVarName: 'RESIN_SUPERVISOR_DELTA_VERSION', varType: 'int', defaultValue: '2' }
|
deltaVersion: { envVarName: 'RESIN_SUPERVISOR_DELTA_VERSION', varType: 'int', defaultValue: '2' }
|
||||||
lockOverride: { envVarName: 'RESIN_SUPERVISOR_OVERRIDE_LOCK', varType: 'bool', defaultValue: 'false' }
|
lockOverride: { envVarName: 'RESIN_SUPERVISOR_OVERRIDE_LOCK', varType: 'bool', defaultValue: 'false' }
|
||||||
nativeLogger: { envVarName: 'RESIN_SUPERVISOR_NATIVE_LOGGER', varType: 'bool', defaultValue: 'true' }
|
|
||||||
persistentLogging: { envVarName: 'RESIN_SUPERVISOR_PERSISTENT_LOGGING', varType: 'bool', defaultValue: 'false', rebootRequired: true }
|
persistentLogging: { envVarName: 'RESIN_SUPERVISOR_PERSISTENT_LOGGING', varType: 'bool', defaultValue: 'false', rebootRequired: true }
|
||||||
}
|
}
|
||||||
@validKeys = [
|
@validKeys = [
|
||||||
|
@ -159,13 +159,11 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@config.on 'change', (changedConfig) =>
|
@config.on 'change', (changedConfig) =>
|
||||||
if changedConfig.loggingEnabled?
|
if changedConfig.loggingEnabled?
|
||||||
@logger.enable(changedConfig.loggingEnabled)
|
@logger.enable(changedConfig.loggingEnabled)
|
||||||
if changedConfig.nativeLogger?
|
|
||||||
@logger.switchBackend(changedConfig.nativeLogger)
|
|
||||||
if changedConfig.apiSecret?
|
if changedConfig.apiSecret?
|
||||||
@reportCurrentState(api_secret: changedConfig.apiSecret)
|
@reportCurrentState(api_secret: changedConfig.apiSecret)
|
||||||
|
|
||||||
@config.getMany([
|
@config.getMany([
|
||||||
'initialConfigSaved', 'listenPort', 'apiSecret', 'osVersion', 'osVariant', 'logsChannelSecret',
|
'initialConfigSaved', 'listenPort', 'apiSecret', 'osVersion', 'osVariant',
|
||||||
'version', 'provisioned', 'apiEndpoint', 'connectivityCheckEnabled', 'legacyAppsPresent'
|
'version', 'provisioned', 'apiEndpoint', 'connectivityCheckEnabled', 'legacyAppsPresent'
|
||||||
])
|
])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
@ -189,7 +187,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
provisioning_progress: null
|
provisioning_progress: null
|
||||||
provisioning_state: ''
|
provisioning_state: ''
|
||||||
status: 'Idle'
|
status: 'Idle'
|
||||||
logs_channel: conf.logsChannelSecret
|
logs_channel: null
|
||||||
update_failed: false
|
update_failed: false
|
||||||
update_pending: false
|
update_pending: false
|
||||||
update_downloaded: false
|
update_downloaded: false
|
||||||
|
@ -26,8 +26,6 @@ const constants = {
|
|||||||
proxyvisorHookReceiver:
|
proxyvisorHookReceiver:
|
||||||
checkString(process.env.RESIN_PROXYVISOR_HOOK_RECEIVER) || 'http://0.0.0.0:1337',
|
checkString(process.env.RESIN_PROXYVISOR_HOOK_RECEIVER) || 'http://0.0.0.0:1337',
|
||||||
configJsonNonAtomicPath: '/boot/config.json',
|
configJsonNonAtomicPath: '/boot/config.json',
|
||||||
defaultPubnubSubscribeKey: process.env.DEFAULT_PUBNUB_SUBSCRIBE_KEY,
|
|
||||||
defaultPubnubPublishKey: process.env.DEFAULT_PUBNUB_PUBLISH_KEY,
|
|
||||||
defaultMixpanelToken: process.env.DEFAULT_MIXPANEL_TOKEN,
|
defaultMixpanelToken: process.env.DEFAULT_MIXPANEL_TOKEN,
|
||||||
supervisorNetworkInterface: supervisorNetworkInterface,
|
supervisorNetworkInterface: supervisorNetworkInterface,
|
||||||
allowedInterfaces: [ 'resin-vpn', 'tun0', 'docker0', 'lo', supervisorNetworkInterface ],
|
allowedInterfaces: [ 'resin-vpn', 'tun0', 'docker0', 'lo', supervisorNetworkInterface ],
|
||||||
|
@ -1,193 +1,171 @@
|
|||||||
|
url = require 'url'
|
||||||
|
https = require 'https'
|
||||||
|
stream = require 'stream'
|
||||||
|
zlib = require 'zlib'
|
||||||
|
|
||||||
_ = require 'lodash'
|
_ = require 'lodash'
|
||||||
PUBNUB = require 'pubnub'
|
|
||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
es = require 'event-stream'
|
es = require 'event-stream'
|
||||||
Lock = require 'rwlock'
|
Lock = require 'rwlock'
|
||||||
{ checkTruthy } = require './lib/validation'
|
{ checkTruthy } = require './lib/validation'
|
||||||
|
|
||||||
class NativeLoggerBackend
|
ZLIB_TIMEOUT = 100
|
||||||
constructor: ->
|
COOLDOWN_PERIOD = 5 * 1000
|
||||||
@logPublishInterval = 1000
|
KEEPALIVE_TIMEOUT = 60 * 1000
|
||||||
@maxLogLinesPerBatch = 10
|
RESPONSE_GRACE_PERIOD = 5 * 1000
|
||||||
@maxQueueSize = 100
|
|
||||||
@maxLineLength = 300
|
MAX_LOG_LENGTH = 10 * 1000
|
||||||
@publishQueue = []
|
MAX_PENDING_BYTES = 256 * 1024
|
||||||
@intervalHandle = null
|
|
||||||
@publishEnabled = false
|
class LogBackend
|
||||||
|
constructor: (apiEndpoint, uuid, deviceApiKey) ->
|
||||||
|
@publishEnabled = true
|
||||||
@offlineMode = false
|
@offlineMode = false
|
||||||
@shouldStop = false
|
|
||||||
|
|
||||||
publishLoop: =>
|
@_req = null
|
||||||
startTime = process.hrtime()
|
@_dropCount = 0
|
||||||
Promise.try =>
|
@_writable = true
|
||||||
return if @offlineMode or !@publishEnabled or @publishQueue.length is 0
|
@_gzip = null
|
||||||
currentBatch = @publishQueue.splice(0, @maxLogLinesPerBatch)
|
|
||||||
# Silently ignore errors sending logs (just like with pubnub)
|
|
||||||
@apiBinder.logBatch(currentBatch)
|
|
||||||
.catchReturn()
|
|
||||||
.then =>
|
|
||||||
elapsedTime = process.hrtime(startTime)
|
|
||||||
elapsedTimeMs = elapsedTime[0] * 1000 + elapsedTime[1] / 1e6
|
|
||||||
nextDelay = Math.max(@logPublishInterval - elapsedTimeMs, 0)
|
|
||||||
Promise.delay(nextDelay)
|
|
||||||
.then =>
|
|
||||||
if !@shouldStop
|
|
||||||
@publishLoop()
|
|
||||||
|
|
||||||
_truncateToMaxLength: (message) =>
|
@_opts = url.parse("#{apiEndpoint}/device/v2/#{uuid}/log-stream")
|
||||||
if message.message.length > @maxLineLength
|
@_opts.method = 'POST'
|
||||||
message = _.clone(message)
|
@_opts.headers = {
|
||||||
message.message = _.truncate(message.message, { length: @maxLineLength, omission: '[...]' })
|
'Authorization': "Bearer #{deviceApiKey}"
|
||||||
return message
|
'Content-Type': 'application/x-ndjson'
|
||||||
|
'Content-Encoding': 'gzip'
|
||||||
|
}
|
||||||
|
|
||||||
logDependent: (msg, { uuid }) =>
|
# This stream serves serves as a message buffer during reconnections
|
||||||
msg = @_truncateToMaxLength(msg)
|
# while we unpipe the old, malfunctioning connection and then repipe a
|
||||||
@apiBinder.logDependent(uuid, msg)
|
# new one.
|
||||||
|
@_stream = new stream.PassThrough({
|
||||||
log: (message) =>
|
allowHalfOpen: true
|
||||||
return if @offlineMode or !@publishEnabled or @publishQueue.length >= @maxQueueSize
|
# We halve the high watermark because a passthrough stream has two
|
||||||
if _.isString(message)
|
# buffers, one for the writable and one for the readable side. The
|
||||||
message = { message: message }
|
# write() call only returns false when both buffers are full.
|
||||||
|
highWaterMark: MAX_PENDING_BYTES / 2
|
||||||
message = _.defaults({}, message, {
|
|
||||||
timestamp: Date.now()
|
|
||||||
message: ''
|
|
||||||
})
|
})
|
||||||
|
@_stream.on 'drain', =>
|
||||||
message = @_truncateToMaxLength(message)
|
@_writable = true
|
||||||
|
@_flush()
|
||||||
if @publishQueue.length < @maxQueueSize - 1
|
if @_dropCount > 0
|
||||||
@publishQueue.push(_.pick(message, [ 'message', 'timestamp', 'isSystem', 'isStderr', 'imageId' ]))
|
@_write({
|
||||||
else
|
message: "Warning: Suppressed #{@_dropCount} message(s) due to high load"
|
||||||
@publishQueue.push({
|
|
||||||
message: 'Warning! Some logs dropped due to high load'
|
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
isSystem: true
|
isSystem: true
|
||||||
isStderr: true
|
isStdErr: true
|
||||||
})
|
})
|
||||||
|
@_dropCount = 0
|
||||||
|
|
||||||
start: (opts) =>
|
@_setup = _.throttle(@_setup, COOLDOWN_PERIOD)
|
||||||
console.log('Starting native logger')
|
@_snooze = _.debounce(@_teardown, KEEPALIVE_TIMEOUT)
|
||||||
@apiBinder = opts.apiBinder
|
|
||||||
@publishEnabled = checkTruthy(opts.enable)
|
|
||||||
@offlineMode = checkTruthy(opts.offlineMode)
|
|
||||||
@publishLoop()
|
|
||||||
return null
|
|
||||||
|
|
||||||
stop: =>
|
# Flushing every ZLIB_TIMEOUT hits a balance between compression and
|
||||||
console.log('Stopping native logger')
|
# latency. When ZLIB_TIMEOUT is 0 the compression ratio is around 5x
|
||||||
@shouldStop = true
|
# whereas when ZLIB_TIMEOUT is infinity the compession ratio is around 10x.
|
||||||
|
@_flush = _.throttle(@_flush, ZLIB_TIMEOUT, leading: false)
|
||||||
|
|
||||||
class PubnubLoggerBackend
|
_setup: ->
|
||||||
constructor: ->
|
@_req = https.request(@_opts)
|
||||||
@publishQueue = [[]]
|
|
||||||
@messageIndex = 0
|
|
||||||
@maxMessageIndex = 9
|
|
||||||
# Pubnub's message size limit is 32KB (unclear on whether it's KB or actually KiB,
|
|
||||||
# but we'll be conservative). So we limit a log message to 2 bytes less to account
|
|
||||||
# for the [ and ] in the array.
|
|
||||||
@maxLogByteSize = 30000
|
|
||||||
@publishQueueRemainingBytes = @maxLogByteSize
|
|
||||||
@logsOverflow = false
|
|
||||||
@logPublishInterval = 110
|
|
||||||
|
|
||||||
doPublish: =>
|
# Since we haven't sent the request body yet, and never will,the
|
||||||
return if @offlineMode or !@publishEnabled or @publishQueue[0].length is 0
|
# only reason for the server to prematurely respond is to
|
||||||
message = @publishQueue.shift()
|
# communicate an error. So teardown the connection immediately
|
||||||
@pubnub.publish({ @channel, message })
|
@_req.on 'response', (res) =>
|
||||||
if @publishQueue.length is 0
|
console.log('LogBackend: server responded with status code:', res.statusCode)
|
||||||
@publishQueue = [[]]
|
@_teardown()
|
||||||
@publishQueueRemainingBytes = @maxLogByteSize
|
|
||||||
@messageIndex = Math.max(@messageIndex - 1, 0)
|
|
||||||
@logsOverflow = false if @messageIndex < @maxMessageIndex
|
|
||||||
|
|
||||||
logDependent: (message, { channel }) ->
|
@_req.on('timeout', => @_teardown())
|
||||||
@pubnub.publish({ channel, message })
|
@_req.on('close', => @_teardown())
|
||||||
|
@_req.on 'error', (err) =>
|
||||||
|
console.log('LogBackend: unexpected error:', err)
|
||||||
|
@_teardown()
|
||||||
|
|
||||||
log: (msg) =>
|
# Immediately flush the headers. This gives a chance to the server to
|
||||||
return if @offlineMode or !@publishEnabled or (@messageIndex >= @maxMessageIndex and @publishQueueRemainingBytes <= 0)
|
# respond with potential errors such as 401 authentication error
|
||||||
if _.isString(message)
|
@_req.flushHeaders()
|
||||||
message = { m: msg }
|
|
||||||
|
# We want a very low writable high watermark to prevent having many
|
||||||
|
# chunks stored in the writable queue of @_gzip and have them in
|
||||||
|
# @_stream instead. This is desirable because once @_gzip.flush() is
|
||||||
|
# called it will do all pending writes with that flush flag. This is
|
||||||
|
# not what we want though. If there are 100 items in the queue we want
|
||||||
|
# to write all of them with Z_NO_FLUSH and only afterwards do a
|
||||||
|
# Z_SYNC_FLUSH to maximize compression
|
||||||
|
@_gzip = zlib.createGzip(writableHighWaterMark: 1024)
|
||||||
|
@_gzip.on('error', => @_teardown())
|
||||||
|
@_gzip.pipe(@_req)
|
||||||
|
|
||||||
|
# Only start piping if there has been no error after the header flush.
|
||||||
|
# Doing it immediately would potentialy lose logs if it turned out that
|
||||||
|
# the server is unavailalbe because @_req stream would consume our
|
||||||
|
# passthrough buffer
|
||||||
|
@_timeout = setTimeout(=>
|
||||||
|
@_stream.pipe(@_gzip)
|
||||||
|
@_flush()
|
||||||
|
, RESPONSE_GRACE_PERIOD)
|
||||||
|
|
||||||
|
_teardown: ->
|
||||||
|
if @_req isnt null
|
||||||
|
clearTimeout(@_timeout)
|
||||||
|
@_req.removeAllListeners()
|
||||||
|
@_req.on('error', _.noop)
|
||||||
|
# no-op if pipe hasn't happened yet
|
||||||
|
@_stream.unpipe(@_gzip)
|
||||||
|
@_gzip.end()
|
||||||
|
@_req = null
|
||||||
|
|
||||||
|
_flush: ->
|
||||||
|
@_gzip.flush(zlib.Z_SYNC_FLUSH)
|
||||||
|
|
||||||
|
_write: (msg) ->
|
||||||
|
@_snooze()
|
||||||
|
|
||||||
|
if @_req is null
|
||||||
|
@_setup()
|
||||||
|
|
||||||
|
if @_writable
|
||||||
|
@_writable = @_stream.write(JSON.stringify(msg) + '\n')
|
||||||
|
@_flush()
|
||||||
else
|
else
|
||||||
message = {
|
@_dropCount += 1
|
||||||
m: msg.message
|
|
||||||
t: msg.timestamp
|
|
||||||
}
|
|
||||||
if msg.isSystem
|
|
||||||
message.s = 1
|
|
||||||
if msg.serviceId?
|
|
||||||
message.c = msg.serviceId
|
|
||||||
if msg.isStderr
|
|
||||||
message.e = 1
|
|
||||||
|
|
||||||
_.defaults message,
|
log: (msg) ->
|
||||||
t: Date.now()
|
if @offlineMode or !@publishEnabled
|
||||||
m: ''
|
return
|
||||||
|
|
||||||
msgLength = Buffer.byteLength(encodeURIComponent(JSON.stringify(message)), 'utf8')
|
if !_.isObject(msg)
|
||||||
return if msgLength > @maxLogByteSize # Unlikely, but we can't allow this
|
return
|
||||||
remaining = @publishQueueRemainingBytes - msgLength
|
|
||||||
if remaining >= 0
|
|
||||||
@publishQueue[@messageIndex].push(message)
|
|
||||||
@publishQueueRemainingBytes = remaining
|
|
||||||
else if @messageIndex < @maxMessageIndex
|
|
||||||
@messageIndex += 1
|
|
||||||
@publishQueue[@messageIndex] = [ message ]
|
|
||||||
@publishQueueRemainingBytes = @maxLogByteSize - msgLength
|
|
||||||
else if !@logsOverflow
|
|
||||||
@logsOverflow = true
|
|
||||||
@messageIndex += 1
|
|
||||||
@publishQueue[@messageIndex] = [ { m: 'Warning! Some logs dropped due to high load', t: Date.now(), s: 1 } ]
|
|
||||||
@publishQueueRemainingBytes = 0
|
|
||||||
|
|
||||||
start: (opts) =>
|
msg = _.assign({
|
||||||
console.log('Starting pubnub logger')
|
timestamp: Date.now()
|
||||||
@pubnub = PUBNUB.init(opts.pubnub)
|
message: ''
|
||||||
@channel = opts.channel
|
}, msg)
|
||||||
@publishEnabled = checkTruthy(opts.enable)
|
|
||||||
@offlineMode = checkTruthy(opts.offlineMode)
|
|
||||||
@intervalHandle = setInterval(@doPublish, @logPublishInterval)
|
|
||||||
|
|
||||||
stop: =>
|
if !(msg.isSystem or msg.serviceId?)
|
||||||
console.log('Stopping pubnub logger')
|
return
|
||||||
clearInterval(@intervalHandle)
|
|
||||||
|
msg.message = _.truncate(msg.message, { length: MAX_LOG_LENGTH, omission: '[...]' })
|
||||||
|
|
||||||
|
@_write(msg)
|
||||||
|
|
||||||
module.exports = class Logger
|
module.exports = class Logger
|
||||||
constructor: ({ @eventTracker }) ->
|
constructor: ({ @eventTracker }) ->
|
||||||
_lock = new Lock()
|
_lock = new Lock()
|
||||||
@_writeLock = Promise.promisify(_lock.async.writeLock)
|
@_writeLock = Promise.promisify(_lock.async.writeLock)
|
||||||
@attached = { stdout: {}, stderr: {} }
|
@attached = { stdout: {}, stderr: {} }
|
||||||
@opts = {}
|
|
||||||
@backend = null
|
@backend = null
|
||||||
|
|
||||||
init: (opts) =>
|
init: (opts) =>
|
||||||
Promise.try =>
|
@backend = new LogBackend(opts.apiEndpoint, opts.uuid, opts.deviceApiKey)
|
||||||
@opts = opts
|
@backend.offlineMode = checkTruthy(opts.offlineMode)
|
||||||
@_startBackend()
|
|
||||||
|
|
||||||
stop: =>
|
|
||||||
if @backend?
|
|
||||||
@backend.stop()
|
|
||||||
|
|
||||||
_startBackend: =>
|
|
||||||
if checkTruthy(@opts.nativeLogger)
|
|
||||||
@backend = new NativeLoggerBackend()
|
|
||||||
else
|
|
||||||
@backend = new PubnubLoggerBackend()
|
|
||||||
@backend.start(@opts)
|
|
||||||
|
|
||||||
switchBackend: (nativeLogger) =>
|
|
||||||
if checkTruthy(nativeLogger) != checkTruthy(@opts.nativeLogger)
|
|
||||||
@opts.nativeLogger = nativeLogger
|
|
||||||
@backend.stop()
|
|
||||||
@_startBackend()
|
|
||||||
|
|
||||||
enable: (val) =>
|
enable: (val) =>
|
||||||
@opts.enable = val
|
|
||||||
@backend.publishEnabled = checkTruthy(val) ? true
|
@backend.publishEnabled = checkTruthy(val) ? true
|
||||||
|
|
||||||
logDependent: (msg, device) =>
|
logDependent: (msg, device) =>
|
||||||
@backend.logDependent(msg, device)
|
msg.uuid = device.uuid
|
||||||
|
@backend.log(msg)
|
||||||
|
|
||||||
log: (msg) =>
|
log: (msg) =>
|
||||||
@backend.log(msg)
|
@backend.log(msg)
|
||||||
@ -195,7 +173,7 @@ module.exports = class Logger
|
|||||||
logSystemMessage: (msg, obj, eventName) =>
|
logSystemMessage: (msg, obj, eventName) =>
|
||||||
messageObj = { message: msg, isSystem: true }
|
messageObj = { message: msg, isSystem: true }
|
||||||
if obj?.error?
|
if obj?.error?
|
||||||
messageObj.isStderr = true
|
messageObj.isStdErr = true
|
||||||
@log(messageObj)
|
@log(messageObj)
|
||||||
@eventTracker.track(eventName ? msg, obj)
|
@eventTracker.track(eventName ? msg, obj)
|
||||||
|
|
||||||
@ -209,7 +187,7 @@ module.exports = class Logger
|
|||||||
if stdoutOrStderr not in [ 'stdout', 'stderr' ]
|
if stdoutOrStderr not in [ 'stdout', 'stderr' ]
|
||||||
throw new Error("Invalid log selection #{stdoutOrStderr}")
|
throw new Error("Invalid log selection #{stdoutOrStderr}")
|
||||||
if !@attached[stdoutOrStderr][containerId]
|
if !@attached[stdoutOrStderr][containerId]
|
||||||
logsOpts = { follow: true, stdout: stdoutOrStderr == 'stdout', stderr: stdoutOrStderr == 'stderr', timestamps: true }
|
logsOpts = { follow: true, stdout: stdoutOrStderr == 'stdout', stderr: stdoutOrStderr == 'stderr', timestamps: true, since: Math.floor(Date.now() / 1000) }
|
||||||
docker.getContainer(containerId)
|
docker.getContainer(containerId)
|
||||||
.logs(logsOpts)
|
.logs(logsOpts)
|
||||||
.then (stream) =>
|
.then (stream) =>
|
||||||
@ -222,9 +200,9 @@ module.exports = class Logger
|
|||||||
.on 'data', (logLine) =>
|
.on 'data', (logLine) =>
|
||||||
space = logLine.indexOf(' ')
|
space = logLine.indexOf(' ')
|
||||||
if space > 0
|
if space > 0
|
||||||
msg = { timestamp: logLine.substr(0, space), message: logLine.substr(space + 1), serviceId, imageId }
|
msg = { timestamp: (new Date(logLine.substr(0, space))).getTime(), message: logLine.substr(space + 1), serviceId, imageId }
|
||||||
if stdoutOrStderr == 'stderr'
|
if stdoutOrStderr == 'stderr'
|
||||||
msg.isStderr = true
|
msg.isStdErr = true
|
||||||
@log(msg)
|
@log(msg)
|
||||||
.on 'error', (err) =>
|
.on 'error', (err) =>
|
||||||
console.error('Error on container logs', err, err.stack)
|
console.error('Error on container logs', err, err.stack)
|
||||||
|
@ -105,7 +105,6 @@ createProxyvisorRouter = (proxyvisor) ->
|
|||||||
deviceId: dev.id
|
deviceId: dev.id
|
||||||
name: dev.name
|
name: dev.name
|
||||||
status: dev.status
|
status: dev.status
|
||||||
logs_channel: dev.logs_channel
|
|
||||||
}
|
}
|
||||||
db.models('dependentDevice').insert(deviceForDB)
|
db.models('dependentDevice').insert(deviceForDB)
|
||||||
.then ->
|
.then ->
|
||||||
@ -137,7 +136,7 @@ createProxyvisorRouter = (proxyvisor) ->
|
|||||||
.then ([ device ]) ->
|
.then ([ device ]) ->
|
||||||
return res.status(404).send('Device not found') if !device?
|
return res.status(404).send('Device not found') if !device?
|
||||||
return res.status(410).send('Device deleted') if device.markedForDeletion
|
return res.status(410).send('Device deleted') if device.markedForDeletion
|
||||||
proxyvisor.logger.logDependent(m, { uuid, channel: "device-#{device.logs_channel}-logs" })
|
proxyvisor.logger.logDependent(m, uuid)
|
||||||
res.status(202).send('OK')
|
res.status(202).send('OK')
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
console.error("Error on #{req.method} #{url.parse(req.url).pathname}", err, err.stack)
|
||||||
@ -260,7 +259,6 @@ module.exports = class Proxyvisor
|
|||||||
is_online: dev.is_online
|
is_online: dev.is_online
|
||||||
name: dev.name
|
name: dev.name
|
||||||
status: dev.status
|
status: dev.status
|
||||||
logs_channel: dev.logs_channel
|
|
||||||
targetCommit
|
targetCommit
|
||||||
targetConfig
|
targetConfig
|
||||||
targetEnvironment
|
targetEnvironment
|
||||||
|
@ -13,15 +13,14 @@ constants = require './lib/constants'
|
|||||||
startupConfigFields = [
|
startupConfigFields = [
|
||||||
'uuid'
|
'uuid'
|
||||||
'listenPort'
|
'listenPort'
|
||||||
|
'apiEndpoint'
|
||||||
'apiSecret'
|
'apiSecret'
|
||||||
'apiTimeout'
|
'apiTimeout'
|
||||||
'offlineMode'
|
'offlineMode'
|
||||||
|
'deviceApiKey'
|
||||||
'mixpanelToken'
|
'mixpanelToken'
|
||||||
'mixpanelHost'
|
'mixpanelHost'
|
||||||
'logsChannelSecret'
|
|
||||||
'pubnub'
|
|
||||||
'loggingEnabled'
|
'loggingEnabled'
|
||||||
'nativeLogger'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
module.exports = class Supervisor extends EventEmitter
|
module.exports = class Supervisor extends EventEmitter
|
||||||
@ -41,7 +40,7 @@ module.exports = class Supervisor extends EventEmitter
|
|||||||
init: =>
|
init: =>
|
||||||
@db.init()
|
@db.init()
|
||||||
.tap =>
|
.tap =>
|
||||||
@config.init() # Ensures uuid, deviceApiKey, apiSecret and logsChannel
|
@config.init() # Ensures uuid, deviceApiKey, apiSecret
|
||||||
.then =>
|
.then =>
|
||||||
@config.getMany(startupConfigFields)
|
@config.getMany(startupConfigFields)
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
@ -52,10 +51,9 @@ module.exports = class Supervisor extends EventEmitter
|
|||||||
@apiBinder.initClient()
|
@apiBinder.initClient()
|
||||||
.then =>
|
.then =>
|
||||||
@logger.init({
|
@logger.init({
|
||||||
nativeLogger: conf.nativeLogger
|
apiEndpoint: conf.apiEndpoint
|
||||||
apiBinder: @apiBinder
|
uuid: conf.uuid
|
||||||
pubnub: conf.pubnub
|
deviceApiKey: conf.deviceApiKey
|
||||||
channel: "device-#{conf.logsChannelSecret}-logs"
|
|
||||||
offlineMode: conf.offlineMode
|
offlineMode: conf.offlineMode
|
||||||
enable: conf.loggingEnabled
|
enable: conf.loggingEnabled
|
||||||
})
|
})
|
||||||
|
@ -35,10 +35,6 @@ describe 'Config', ->
|
|||||||
promise = @conf.getMany([ 'applicationId', 'apiEndpoint' ])
|
promise = @conf.getMany([ 'applicationId', 'apiEndpoint' ])
|
||||||
expect(promise).to.eventually.deep.equal({ applicationId: 78373, apiEndpoint: 'https://api.resin.io' })
|
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', ->
|
it 'generates a uuid and stores it in config.json', ->
|
||||||
promise = @conf.get('uuid')
|
promise = @conf.get('uuid')
|
||||||
promise2 = fs.readFileAsync('./test/data/config.json').then(JSON.parse).get('uuid')
|
promise2 = fs.readFileAsync('./test/data/config.json').then(JSON.parse).get('uuid')
|
||||||
@ -122,15 +118,6 @@ describe 'Config', ->
|
|||||||
@conf = new Config({ @db })
|
@conf = new Config({ @db })
|
||||||
@initialization = @db.init().then =>
|
@initialization = @db.init().then =>
|
||||||
@conf.init()
|
@conf.init()
|
||||||
it 'should allow setting of mutable function config options', ->
|
|
||||||
@conf.set({ logsChannelSecret: 'test' })
|
|
||||||
.then =>
|
|
||||||
expect(@conf.get('logsChannelSecret')).to.eventually.equal('test')
|
|
||||||
|
|
||||||
it 'should allow removing of mutabe function config options', ->
|
|
||||||
@conf.remove('logsChannelSecret')
|
|
||||||
.then =>
|
|
||||||
expect(@conf.get('logsChannelSecret')).to.eventually.be.undefined
|
|
||||||
|
|
||||||
it 'should throw if a non-mutable function provider is set', ->
|
it 'should throw if a non-mutable function provider is set', ->
|
||||||
expect(@conf.set({ version: 'some-version' })).to.be.rejected
|
expect(@conf.set({ version: 'some-version' })).to.be.rejected
|
||||||
|
@ -42,7 +42,6 @@ testTarget1 = {
|
|||||||
'RESIN_SUPERVISOR_DELTA_VERSION': '2'
|
'RESIN_SUPERVISOR_DELTA_VERSION': '2'
|
||||||
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||||
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||||
'RESIN_SUPERVISOR_NATIVE_LOGGER': 'true'
|
|
||||||
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||||
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||||
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
||||||
@ -125,7 +124,6 @@ testTargetWithDefaults2 = {
|
|||||||
'RESIN_SUPERVISOR_DELTA_VERSION': '2'
|
'RESIN_SUPERVISOR_DELTA_VERSION': '2'
|
||||||
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||||
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||||
'RESIN_SUPERVISOR_NATIVE_LOGGER': 'true'
|
|
||||||
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||||
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||||
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
||||||
|
@ -1,39 +1,106 @@
|
|||||||
|
https = require 'https'
|
||||||
|
stream = require 'stream'
|
||||||
|
zlib = require 'zlib'
|
||||||
|
|
||||||
|
Promise = require 'bluebird'
|
||||||
m = require 'mochainon'
|
m = require 'mochainon'
|
||||||
{ expect } = m.chai
|
{ expect } = m.chai
|
||||||
{ spy, useFakeTimers } = m.sinon
|
{ stub } = m.sinon
|
||||||
|
|
||||||
Logger = require '../src/logger'
|
Logger = require '../src/logger'
|
||||||
|
|
||||||
describe 'Logger', ->
|
describe 'Logger', ->
|
||||||
before ->
|
beforeEach ->
|
||||||
@fakeBinder = {
|
@_req = new stream.PassThrough()
|
||||||
logBatch: spy()
|
@_req.flushHeaders = m.sinon.spy()
|
||||||
}
|
@_req.end = m.sinon.spy()
|
||||||
|
|
||||||
|
@_req.body = ''
|
||||||
|
@_req
|
||||||
|
.pipe(zlib.createGunzip())
|
||||||
|
.on 'data', (chunk) =>
|
||||||
|
@_req.body += chunk
|
||||||
|
|
||||||
|
stub(https, 'request').returns(@_req)
|
||||||
|
|
||||||
@fakeEventTracker = {
|
@fakeEventTracker = {
|
||||||
track: spy()
|
track: m.sinon.spy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@logger = new Logger({eventTracker: @fakeEventTracker})
|
@logger = new Logger({eventTracker: @fakeEventTracker})
|
||||||
@logger.init({ pubnub: {}, channel: 'foo', offlineMode: 'false', enable: 'true', nativeLogger: 'true', apiBinder: @fakeBinder })
|
@logger.init({
|
||||||
|
apiEndpoint: 'https://example.com'
|
||||||
|
uuid: 'deadbeef'
|
||||||
|
deviceApiKey: 'secretkey'
|
||||||
|
offlineMode: false
|
||||||
|
})
|
||||||
|
|
||||||
after ->
|
afterEach ->
|
||||||
@logger.stop()
|
https.request.restore()
|
||||||
|
|
||||||
it 'publishes logs to the resin API by default', (done) ->
|
it 'waits the grace period before sending any logs', ->
|
||||||
theTime = Date.now()
|
clock = m.sinon.useFakeTimers()
|
||||||
@logger.log(message: 'Hello!', timestamp: theTime)
|
@logger.log({message: 'foobar', serviceId: 15})
|
||||||
setTimeout( =>
|
clock.tick(4999)
|
||||||
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()
|
clock.restore()
|
||||||
setTimeout( =>
|
|
||||||
expect(@fakeBinder.logBatch).to.be.calledWith([ { message: 'Hello there!', timestamp: 10, isSystem: true } ])
|
Promise.delay(10)
|
||||||
|
.then =>
|
||||||
|
expect(@_req.body).to.equal('')
|
||||||
|
|
||||||
|
it 'tears down the connection after inactivity', ->
|
||||||
|
clock = m.sinon.useFakeTimers()
|
||||||
|
@logger.log({message: 'foobar', serviceId: 15})
|
||||||
|
clock.tick(61000)
|
||||||
|
clock.restore()
|
||||||
|
|
||||||
|
Promise.delay(10)
|
||||||
|
.then =>
|
||||||
|
expect(@_req.end.calledOnce).to.be.true
|
||||||
|
|
||||||
|
|
||||||
|
it 'sends logs as gzipped ndjson', ->
|
||||||
|
clock = m.sinon.useFakeTimers()
|
||||||
|
@logger.log({ message: 'foobar', serviceId: 15 })
|
||||||
|
@logger.log({ timestamp: 1337, message: 'foobar', serviceId: 15 })
|
||||||
|
@logger.log({ message: 'foobar' }) # shold be ignored
|
||||||
|
clock.tick(10000)
|
||||||
|
clock.restore()
|
||||||
|
|
||||||
|
expect(https.request.calledOnce).to.be.true
|
||||||
|
opts = https.request.firstCall.args[0]
|
||||||
|
|
||||||
|
expect(opts.href).to.equal('https://example.com/device/v2/deadbeef/log-stream')
|
||||||
|
expect(opts.method).to.equal('POST')
|
||||||
|
expect(opts.headers).to.deep.equal({
|
||||||
|
'Authorization': 'Bearer secretkey'
|
||||||
|
'Content-Type': 'application/x-ndjson'
|
||||||
|
'Content-Encoding': 'gzip'
|
||||||
|
})
|
||||||
|
|
||||||
|
# small delay for the streams to propagate data
|
||||||
|
Promise.delay(10)
|
||||||
|
.then =>
|
||||||
|
lines = @_req.body.split('\n')
|
||||||
|
expect(lines.length).to.equal(3)
|
||||||
|
expect(lines[2]).to.equal('')
|
||||||
|
|
||||||
|
msg = JSON.parse(lines[0])
|
||||||
|
expect(msg).to.deep.equal({ timestamp: 0, message: 'foobar', serviceId: 15 })
|
||||||
|
msg = JSON.parse(lines[1])
|
||||||
|
expect(msg).to.deep.equal({ timestamp: 1337, message: 'foobar', serviceId: 15 })
|
||||||
|
|
||||||
|
it 'allows logging system messages which are also reported to the eventTracker', ->
|
||||||
|
clock = m.sinon.useFakeTimers()
|
||||||
|
@logger.logSystemMessage('Hello there!', { someProp: 'someVal' }, 'Some event name')
|
||||||
|
clock.tick(10000)
|
||||||
|
clock.restore()
|
||||||
|
|
||||||
|
Promise.delay(10)
|
||||||
|
.then =>
|
||||||
expect(@fakeEventTracker.track).to.be.calledWith('Some event name', { someProp: 'someVal' })
|
expect(@fakeEventTracker.track).to.be.calledWith('Some event name', { someProp: 'someVal' })
|
||||||
done()
|
lines = @_req.body.split('\n')
|
||||||
, 1020)
|
expect(lines.length).to.equal(2)
|
||||||
|
expect(lines[1]).to.equal('')
|
||||||
|
|
||||||
|
msg = JSON.parse(lines[0])
|
||||||
|
expect(msg).to.deep.equal({ message: 'Hello there!', timestamp: 0, isSystem: true })
|
||||||
|
@ -1 +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","supervisorOfflineMode":true}
|
{"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","mixpanelToken":"baz","apiKey":"boo","version":"2.0.6+rev3.prod","supervisorOfflineMode":true}
|
||||||
|
@ -1 +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"}
|
{"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","mixpanelToken":"baz","apiKey":"boo","version":"2.0.6+rev3.prod"}
|
||||||
|
@ -1 +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"}
|
{"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","mixpanelToken":"baz","apiKey":"boo","version":"2.0.6+rev3.prod"}
|
||||||
|
@ -42,8 +42,6 @@ runSupervisor() {
|
|||||||
-e BOOT_MOUNTPOINT=$BOOT_MOUNTPOINT \
|
-e BOOT_MOUNTPOINT=$BOOT_MOUNTPOINT \
|
||||||
-e API_ENDPOINT=$API_ENDPOINT \
|
-e API_ENDPOINT=$API_ENDPOINT \
|
||||||
-e REGISTRY_ENDPOINT=$REGISTRY_ENDPOINT \
|
-e REGISTRY_ENDPOINT=$REGISTRY_ENDPOINT \
|
||||||
-e PUBNUB_SUBSCRIBE_KEY=$PUBNUB_SUBSCRIBE_KEY \
|
|
||||||
-e PUBNUB_PUBLISH_KEY=$PUBNUB_PUBLISH_KEY \
|
|
||||||
-e MIXPANEL_TOKEN=$MIXPANEL_TOKEN \
|
-e MIXPANEL_TOKEN=$MIXPANEL_TOKEN \
|
||||||
-e DELTA_ENDPOINT=$DELTA_ENDPOINT \
|
-e DELTA_ENDPOINT=$DELTA_ENDPOINT \
|
||||||
-e LED_FILE=${LED_FILE} \
|
-e LED_FILE=${LED_FILE} \
|
||||||
|
Loading…
Reference in New Issue
Block a user