mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-25 05:19:59 +00:00
Merge pull request #844 from balena-io/local-mode-unmanaged
Unmanaged + local mode fixes
This commit is contained in:
commit
d3d0e19a16
@ -57,9 +57,9 @@ module.exports = class APIBinder
|
|||||||
@readyForUpdates = false
|
@readyForUpdates = false
|
||||||
|
|
||||||
healthcheck: =>
|
healthcheck: =>
|
||||||
@config.getMany([ 'appUpdatePollInterval', 'offlineMode', 'connectivityCheckEnabled' ])
|
@config.getMany([ 'appUpdatePollInterval', 'unmanaged', 'connectivityCheckEnabled' ])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
if conf.offlineMode
|
if conf.unmanaged
|
||||||
return true
|
return true
|
||||||
timeSinceLastFetch = process.hrtime(@lastTargetStateFetch)
|
timeSinceLastFetch = process.hrtime(@lastTargetStateFetch)
|
||||||
timeSinceLastFetchMs = timeSinceLastFetch[0] * 1000 + timeSinceLastFetch[1] / 1e6
|
timeSinceLastFetchMs = timeSinceLastFetch[0] * 1000 + timeSinceLastFetch[1] / 1e6
|
||||||
@ -72,10 +72,10 @@ module.exports = class APIBinder
|
|||||||
release()
|
release()
|
||||||
|
|
||||||
initClient: =>
|
initClient: =>
|
||||||
@config.getMany([ 'offlineMode', 'apiEndpoint', 'currentApiKey' ])
|
@config.getMany([ 'unmanaged', 'apiEndpoint', 'currentApiKey' ])
|
||||||
.then ({ offlineMode, apiEndpoint, currentApiKey }) =>
|
.then ({ unmanaged, apiEndpoint, currentApiKey }) =>
|
||||||
if offlineMode
|
if unmanaged
|
||||||
console.log('Offline Mode is set, skipping API client initialization')
|
console.log('Unmanaged Mode is set, skipping API client initialization')
|
||||||
return
|
return
|
||||||
baseUrl = url.resolve(apiEndpoint, '/v5/')
|
baseUrl = url.resolve(apiEndpoint, '/v5/')
|
||||||
passthrough = _.cloneDeep(requestOpts)
|
passthrough = _.cloneDeep(requestOpts)
|
||||||
@ -102,10 +102,10 @@ module.exports = class APIBinder
|
|||||||
@loadBackupFromMigration(retryDelay)
|
@loadBackupFromMigration(retryDelay)
|
||||||
|
|
||||||
start: =>
|
start: =>
|
||||||
@config.getMany([ 'apiEndpoint', 'offlineMode', 'bootstrapRetryDelay' ])
|
@config.getMany([ 'apiEndpoint', 'unmanaged', 'bootstrapRetryDelay' ])
|
||||||
.then ({ apiEndpoint, offlineMode, bootstrapRetryDelay }) =>
|
.then ({ apiEndpoint, unmanaged, bootstrapRetryDelay }) =>
|
||||||
if offlineMode
|
if unmanaged
|
||||||
console.log('Offline Mode is set, skipping API binder initialization')
|
console.log('Unmanaged Mode is set, skipping API binder initialization')
|
||||||
# If we are offline because there is no apiEndpoint, there's a chance
|
# If we are offline because there is no apiEndpoint, there's a chance
|
||||||
# we've went through a deprovision. We need to set the initialConfigReported
|
# we've went through a deprovision. We need to set the initialConfigReported
|
||||||
# value to '', to ensure that when we do re-provision, we'll report
|
# value to '', to ensure that when we do re-provision, we'll report
|
||||||
@ -258,15 +258,15 @@ module.exports = class APIBinder
|
|||||||
|
|
||||||
provisionDependentDevice: (device) =>
|
provisionDependentDevice: (device) =>
|
||||||
@config.getMany([
|
@config.getMany([
|
||||||
'offlineMode'
|
'unmanaged'
|
||||||
'provisioned'
|
'provisioned'
|
||||||
'apiTimeout'
|
'apiTimeout'
|
||||||
'userId'
|
'userId'
|
||||||
'deviceId'
|
'deviceId'
|
||||||
])
|
])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
if conf.offlineMode
|
if conf.unmanaged
|
||||||
throw new Error('Cannot provision dependent device in offline mode')
|
throw new Error('Cannot provision dependent device in unmanaged mode')
|
||||||
if !conf.provisioned
|
if !conf.provisioned
|
||||||
throw new Error('Device must be provisioned to provision a dependent device')
|
throw new Error('Device must be provisioned to provision a dependent device')
|
||||||
# TODO: when API supports it as per https://github.com/resin-io/hq/pull/949 remove userId
|
# TODO: when API supports it as per https://github.com/resin-io/hq/pull/949 remove userId
|
||||||
@ -283,13 +283,13 @@ module.exports = class APIBinder
|
|||||||
|
|
||||||
patchDevice: (id, updatedFields) =>
|
patchDevice: (id, updatedFields) =>
|
||||||
@config.getMany([
|
@config.getMany([
|
||||||
'offlineMode'
|
'unmanaged'
|
||||||
'provisioned'
|
'provisioned'
|
||||||
'apiTimeout'
|
'apiTimeout'
|
||||||
])
|
])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
if conf.offlineMode
|
if conf.unmanaged
|
||||||
throw new Error('Cannot update dependent device in offline mode')
|
throw new Error('Cannot update dependent device in unmanaged mode')
|
||||||
if !conf.provisioned
|
if !conf.provisioned
|
||||||
throw new Error('Device must be provisioned to update a dependent device')
|
throw new Error('Device must be provisioned to update a dependent device')
|
||||||
@balenaApi.patch
|
@balenaApi.patch
|
||||||
@ -338,6 +338,9 @@ module.exports = class APIBinder
|
|||||||
(currentState, targetConfig, defaultConfig, deviceId) =>
|
(currentState, targetConfig, defaultConfig, deviceId) =>
|
||||||
currentConfig = currentState.local.config
|
currentConfig = currentState.local.config
|
||||||
Promise.mapSeries _.toPairs(currentConfig), ([ key, value ]) =>
|
Promise.mapSeries _.toPairs(currentConfig), ([ key, value ]) =>
|
||||||
|
# We want to disable local mode when joining a cloud
|
||||||
|
if key == 'SUPERVISOR_LOCAL_MODE'
|
||||||
|
value = 'false'
|
||||||
# We never want to disable VPN if, for instance, it failed to start so far
|
# We never want to disable VPN if, for instance, it failed to start so far
|
||||||
if key == 'SUPERVISOR_VPN_CONTROL'
|
if key == 'SUPERVISOR_VPN_CONTROL'
|
||||||
value = 'true'
|
value = 'true'
|
||||||
|
@ -9,7 +9,6 @@ import supervisorVersion = require('../lib/supervisor-version');
|
|||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
import * as osRelease from '../lib/os-release';
|
import * as osRelease from '../lib/os-release';
|
||||||
import { ConfigValue } from '../lib/types';
|
import { ConfigValue } from '../lib/types';
|
||||||
import { checkTruthy } from '../lib/validation';
|
|
||||||
|
|
||||||
// A provider for schema entries with source 'func'
|
// A provider for schema entries with source 'func'
|
||||||
type ConfigProviderFunctionGetter = () => Bluebird<any>;
|
type ConfigProviderFunctionGetter = () => Bluebird<any>;
|
||||||
@ -47,17 +46,6 @@ export function createProviderFunctions(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
offlineMode: {
|
|
||||||
get: () => {
|
|
||||||
return config
|
|
||||||
.getMany(['apiEndpoint', 'supervisorOfflineMode'])
|
|
||||||
.then(({ apiEndpoint, supervisorOfflineMode }) => {
|
|
||||||
return (
|
|
||||||
checkTruthy(supervisorOfflineMode as boolean) || !apiEndpoint
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
provisioned: {
|
provisioned: {
|
||||||
get: () => {
|
get: () => {
|
||||||
return config
|
return config
|
||||||
@ -149,5 +137,12 @@ export function createProviderFunctions(
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
unmanaged: {
|
||||||
|
get: () => {
|
||||||
|
return config.get('apiEndpoint').then(apiEndpoint => {
|
||||||
|
return !apiEndpoint;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -46,14 +46,11 @@ class Config extends EventEmitter {
|
|||||||
default: constants.defaultMixpanelToken,
|
default: constants.defaultMixpanelToken,
|
||||||
},
|
},
|
||||||
bootstrapRetryDelay: { source: 'config.json', default: 30000 },
|
bootstrapRetryDelay: { source: 'config.json', default: 30000 },
|
||||||
supervisorOfflineMode: { source: 'config.json', default: false },
|
|
||||||
hostname: { source: 'config.json', mutable: true },
|
hostname: { source: 'config.json', mutable: true },
|
||||||
persistentLogging: { source: 'config.json', default: false, mutable: true },
|
persistentLogging: { source: 'config.json', default: false, mutable: true },
|
||||||
localMode: { source: 'config.json', mutable: true, default: 'false' },
|
|
||||||
|
|
||||||
version: { source: 'func' },
|
version: { source: 'func' },
|
||||||
currentApiKey: { source: 'func' },
|
currentApiKey: { source: 'func' },
|
||||||
offlineMode: { source: 'func' },
|
|
||||||
provisioned: { source: 'func' },
|
provisioned: { source: 'func' },
|
||||||
osVersion: { source: 'func' },
|
osVersion: { source: 'func' },
|
||||||
osVariant: { source: 'func' },
|
osVariant: { source: 'func' },
|
||||||
@ -61,6 +58,7 @@ class Config extends EventEmitter {
|
|||||||
mixpanelHost: { source: 'func' },
|
mixpanelHost: { source: 'func' },
|
||||||
extendedEnvOptions: { source: 'func' },
|
extendedEnvOptions: { source: 'func' },
|
||||||
fetchOptions: { source: 'func' },
|
fetchOptions: { source: 'func' },
|
||||||
|
unmanaged: { source: 'func' },
|
||||||
|
|
||||||
// NOTE: all 'db' values are stored and loaded as *strings*,
|
// NOTE: all 'db' values are stored and loaded as *strings*,
|
||||||
apiSecret: { source: 'db', mutable: true },
|
apiSecret: { source: 'db', mutable: true },
|
||||||
@ -82,6 +80,7 @@ class Config extends EventEmitter {
|
|||||||
pinDevice: { source: 'db', mutable: true, default: 'null' },
|
pinDevice: { source: 'db', mutable: true, default: 'null' },
|
||||||
currentCommit: { source: 'db', mutable: true },
|
currentCommit: { source: 'db', mutable: true },
|
||||||
targetStateSet: { source: 'db', mutable: true, default: 'false' },
|
targetStateSet: { source: 'db', mutable: true, default: 'false' },
|
||||||
|
localMode: { source: 'db', mutable: true, default: 'false' },
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor({ db, configPath }: ConfigOpts) {
|
public constructor({ db, configPath }: ConfigOpts) {
|
||||||
@ -187,7 +186,7 @@ class Config extends EventEmitter {
|
|||||||
if (oldValues[key] !== value) {
|
if (oldValues[key] !== value) {
|
||||||
return this.db.upsertModel(
|
return this.db.upsertModel(
|
||||||
'config',
|
'config',
|
||||||
{ key, value },
|
{ key, value: (value || '').toString() },
|
||||||
{ key },
|
{ key },
|
||||||
tx,
|
tx,
|
||||||
);
|
);
|
||||||
@ -281,15 +280,15 @@ class Config extends EventEmitter {
|
|||||||
'uuid',
|
'uuid',
|
||||||
'deviceApiKey',
|
'deviceApiKey',
|
||||||
'apiSecret',
|
'apiSecret',
|
||||||
'offlineMode',
|
'unmanaged',
|
||||||
]).then(({ uuid, deviceApiKey, apiSecret, offlineMode }) => {
|
]).then(({ uuid, deviceApiKey, apiSecret, unmanaged }) => {
|
||||||
// These fields need to be set regardless
|
// These fields need to be set regardless
|
||||||
if (uuid == null || apiSecret == null) {
|
if (uuid == null || apiSecret == null) {
|
||||||
uuid = uuid || this.newUniqueKey();
|
uuid = uuid || this.newUniqueKey();
|
||||||
apiSecret = apiSecret || this.newUniqueKey();
|
apiSecret = apiSecret || this.newUniqueKey();
|
||||||
}
|
}
|
||||||
return this.set({ uuid, apiSecret }).then(() => {
|
return this.set({ uuid, apiSecret }).then(() => {
|
||||||
if (offlineMode) {
|
if (unmanaged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!deviceApiKey) {
|
if (!deviceApiKey) {
|
||||||
|
@ -13,6 +13,7 @@ module.exports = class DeviceConfig
|
|||||||
@rebootRequired = false
|
@rebootRequired = false
|
||||||
@configKeys = {
|
@configKeys = {
|
||||||
appUpdatePollInterval: { envVarName: 'SUPERVISOR_POLL_INTERVAL', varType: 'int', defaultValue: '60000' }
|
appUpdatePollInterval: { envVarName: 'SUPERVISOR_POLL_INTERVAL', varType: 'int', defaultValue: '60000' }
|
||||||
|
localMode: { envVarName: 'SUPERVISOR_LOCAL_MODE', varType: 'bool', defaultValue: 'false' }
|
||||||
connectivityCheckEnabled: { envVarName: 'SUPERVISOR_CONNECTIVITY_CHECK', varType: 'bool', defaultValue: 'true' }
|
connectivityCheckEnabled: { envVarName: 'SUPERVISOR_CONNECTIVITY_CHECK', varType: 'bool', defaultValue: 'true' }
|
||||||
loggingEnabled: { envVarName: 'SUPERVISOR_LOG_CONTROL', varType: 'bool', defaultValue: 'true' }
|
loggingEnabled: { envVarName: 'SUPERVISOR_LOG_CONTROL', varType: 'bool', defaultValue: 'true' }
|
||||||
delta: { envVarName: 'SUPERVISOR_DELTA', varType: 'bool', defaultValue: 'false' }
|
delta: { envVarName: 'SUPERVISOR_DELTA', varType: 'bool', defaultValue: 'false' }
|
||||||
@ -74,11 +75,16 @@ module.exports = class DeviceConfig
|
|||||||
db('deviceConfig').update(confToUpdate)
|
db('deviceConfig').update(confToUpdate)
|
||||||
|
|
||||||
getTarget: ({ initial = false } = {}) =>
|
getTarget: ({ initial = false } = {}) =>
|
||||||
@db.models('deviceConfig').select('targetValues')
|
Promise.all([
|
||||||
.then ([ devConfig ]) =>
|
@config.get('unmanaged')
|
||||||
|
@db.models('deviceConfig').select('targetValues')
|
||||||
|
])
|
||||||
|
.then ([unmanaged, [ devConfig ]]) =>
|
||||||
conf = JSON.parse(devConfig.targetValues)
|
conf = JSON.parse(devConfig.targetValues)
|
||||||
if initial or !conf.SUPERVISOR_VPN_CONTROL?
|
if initial or !conf.SUPERVISOR_VPN_CONTROL?
|
||||||
conf.SUPERVISOR_VPN_CONTROL = 'true'
|
conf.SUPERVISOR_VPN_CONTROL = 'true'
|
||||||
|
if unmanaged and !conf.SUPERVISOR_LOCAL_MODE?
|
||||||
|
conf.SUPERVISOR_LOCAL_MODE = 'true'
|
||||||
for own k, { envVarName, defaultValue } of @configKeys
|
for own k, { envVarName, defaultValue } of @configKeys
|
||||||
conf[envVarName] ?= defaultValue
|
conf[envVarName] ?= defaultValue
|
||||||
return conf
|
return conf
|
||||||
@ -131,10 +137,10 @@ module.exports = class DeviceConfig
|
|||||||
target = _.clone(targetState.local?.config ? {})
|
target = _.clone(targetState.local?.config ? {})
|
||||||
steps = []
|
steps = []
|
||||||
Promise.all [
|
Promise.all [
|
||||||
@config.getMany([ 'deviceType', 'offlineMode' ])
|
@config.getMany([ 'deviceType', 'unmanaged' ])
|
||||||
@getConfigBackend()
|
@getConfigBackend()
|
||||||
]
|
]
|
||||||
.then ([{ deviceType, offlineMode }, configBackend ]) =>
|
.then ([{ deviceType, unmanaged }, configBackend ]) =>
|
||||||
configChanges = {}
|
configChanges = {}
|
||||||
humanReadableConfigChanges = {}
|
humanReadableConfigChanges = {}
|
||||||
match = {
|
match = {
|
||||||
@ -162,7 +168,7 @@ module.exports = class DeviceConfig
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Check if we need to perform special case actions for the VPN
|
# Check if we need to perform special case actions for the VPN
|
||||||
if !checkTruthy(offlineMode) &&
|
if !checkTruthy(unmanaged) &&
|
||||||
!_.isEmpty(target['SUPERVISOR_VPN_CONTROL']) &&
|
!_.isEmpty(target['SUPERVISOR_VPN_CONTROL']) &&
|
||||||
@checkBoolChanged(current, target, 'SUPERVISOR_VPN_CONTROL')
|
@checkBoolChanged(current, target, 'SUPERVISOR_VPN_CONTROL')
|
||||||
steps.push({
|
steps.push({
|
||||||
|
@ -140,12 +140,12 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@applications.on('change', @reportCurrentState)
|
@applications.on('change', @reportCurrentState)
|
||||||
|
|
||||||
healthcheck: =>
|
healthcheck: =>
|
||||||
@config.getMany([ 'appUpdatePollInterval', 'offlineMode' ])
|
@config.getMany([ 'appUpdatePollInterval', 'unmanaged' ])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
cycleTime = process.hrtime(@lastApplyStart)
|
cycleTime = process.hrtime(@lastApplyStart)
|
||||||
cycleTimeMs = cycleTime[0] * 1000 + cycleTime[1] / 1e6
|
cycleTimeMs = cycleTime[0] * 1000 + cycleTime[1] / 1e6
|
||||||
cycleTimeWithinInterval = cycleTimeMs - @applications.timeSpentFetching < 2 * conf.appUpdatePollInterval
|
cycleTimeWithinInterval = cycleTimeMs - @applications.timeSpentFetching < 2 * conf.appUpdatePollInterval
|
||||||
applyTargetHealthy = conf.offlineMode or !@applyInProgress or @applications.fetchesInProgress > 0 or cycleTimeWithinInterval
|
applyTargetHealthy = conf.unmanaged or !@applyInProgress or @applications.fetchesInProgress > 0 or cycleTimeWithinInterval
|
||||||
return applyTargetHealthy
|
return applyTargetHealthy
|
||||||
|
|
||||||
migrateLegacyApps: (balenaApi) =>
|
migrateLegacyApps: (balenaApi) =>
|
||||||
@ -255,7 +255,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@config.getMany([
|
@config.getMany([
|
||||||
'initialConfigSaved', 'listenPort', 'apiSecret', 'osVersion', 'osVariant',
|
'initialConfigSaved', 'listenPort', 'apiSecret', 'osVersion', 'osVariant',
|
||||||
'version', 'provisioned', 'apiEndpoint', 'connectivityCheckEnabled', 'legacyAppsPresent',
|
'version', 'provisioned', 'apiEndpoint', 'connectivityCheckEnabled', 'legacyAppsPresent',
|
||||||
'targetStateSet', 'offlineMode'
|
'targetStateSet', 'unmanaged'
|
||||||
])
|
])
|
||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
@applications.init()
|
@applications.init()
|
||||||
@ -297,8 +297,8 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
.then =>
|
.then =>
|
||||||
@triggerApplyTarget({ initial: true })
|
@triggerApplyTarget({ initial: true })
|
||||||
|
|
||||||
initNetworkChecks: ({ apiEndpoint, connectivityCheckEnabled, offlineMode }) =>
|
initNetworkChecks: ({ apiEndpoint, connectivityCheckEnabled, unmanaged }) =>
|
||||||
return if validation.checkTruthy(offlineMode)
|
return if validation.checkTruthy(unmanaged)
|
||||||
network.startConnectivityCheck apiEndpoint, connectivityCheckEnabled, (connected) =>
|
network.startConnectivityCheck apiEndpoint, connectivityCheckEnabled, (connected) =>
|
||||||
@connected = connected
|
@connected = connected
|
||||||
@config.on 'change', (changedConfig) ->
|
@config.on 'change', (changedConfig) ->
|
||||||
@ -351,7 +351,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
.then =>
|
.then =>
|
||||||
@deviceConfig.setTarget(target.local.config, trx)
|
@deviceConfig.setTarget(target.local.config, trx)
|
||||||
.then =>
|
.then =>
|
||||||
if localSource
|
if localSource or not apiEndpoint
|
||||||
@applications.setTarget(target.local.apps, target.dependent, 'local', trx)
|
@applications.setTarget(target.local.apps, target.dependent, 'local', trx)
|
||||||
else
|
else
|
||||||
@applications.setTarget(target.local.apps, target.dependent, apiEndpoint, trx)
|
@applications.setTarget(target.local.apps, target.dependent, apiEndpoint, trx)
|
||||||
|
@ -11,7 +11,7 @@ export type EventTrackProperties = Dictionary<any>;
|
|||||||
|
|
||||||
interface InitArgs {
|
interface InitArgs {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
offlineMode: boolean;
|
unmanaged: boolean;
|
||||||
mixpanelHost: { host: string; path: string } | null;
|
mixpanelHost: { host: string; path: string } | null;
|
||||||
mixpanelToken: string;
|
mixpanelToken: string;
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ export class EventTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public init({
|
public init({
|
||||||
offlineMode,
|
unmanaged,
|
||||||
mixpanelHost,
|
mixpanelHost,
|
||||||
mixpanelToken,
|
mixpanelToken,
|
||||||
uuid,
|
uuid,
|
||||||
@ -52,7 +52,7 @@ export class EventTracker {
|
|||||||
uuid,
|
uuid,
|
||||||
supervisorVersion,
|
supervisorVersion,
|
||||||
};
|
};
|
||||||
if (offlineMode || mixpanelHost == null) {
|
if (unmanaged || mixpanelHost == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.client = Mixpanel.init(mixpanelToken, {
|
this.client = Mixpanel.init(mixpanelToken, {
|
||||||
|
@ -40,9 +40,20 @@ export class LocalModeManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const localMode = checkTruthy(
|
// On startup, check if we're in unmanaged mode,
|
||||||
(await this.config.get('localMode')) || false,
|
// as local mode needs to be set
|
||||||
);
|
let unmanagedLocalMode = false;
|
||||||
|
if (checkTruthy((await this.config.get('unmanaged')) || false)) {
|
||||||
|
console.log('Starting up in unmanaged mode, activating local mode');
|
||||||
|
await this.config.set({ localMode: true });
|
||||||
|
unmanagedLocalMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localMode =
|
||||||
|
// short circuit the next get if we know we're in local mode
|
||||||
|
unmanagedLocalMode ||
|
||||||
|
checkTruthy((await this.config.get('localMode')) || false);
|
||||||
|
|
||||||
if (!localMode) {
|
if (!localMode) {
|
||||||
// Remove any leftovers if necessary
|
// Remove any leftovers if necessary
|
||||||
await this.removeLocalModeArtifacts();
|
await this.removeLocalModeArtifacts();
|
||||||
|
@ -17,7 +17,7 @@ interface LoggerSetupOptions {
|
|||||||
apiEndpoint: string;
|
apiEndpoint: string;
|
||||||
uuid: string;
|
uuid: string;
|
||||||
deviceApiKey: string;
|
deviceApiKey: string;
|
||||||
offlineMode: boolean;
|
unmanaged: boolean;
|
||||||
enableLogs: boolean;
|
enableLogs: boolean;
|
||||||
localMode: boolean;
|
localMode: boolean;
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ export class Logger {
|
|||||||
apiEndpoint,
|
apiEndpoint,
|
||||||
uuid,
|
uuid,
|
||||||
deviceApiKey,
|
deviceApiKey,
|
||||||
offlineMode,
|
unmanaged,
|
||||||
enableLogs,
|
enableLogs,
|
||||||
localMode,
|
localMode,
|
||||||
}: LoggerSetupOptions) {
|
}: LoggerSetupOptions) {
|
||||||
@ -68,7 +68,7 @@ export class Logger {
|
|||||||
|
|
||||||
this.backend = localMode ? this.localBackend : this.balenaBackend;
|
this.backend = localMode ? this.localBackend : this.balenaBackend;
|
||||||
|
|
||||||
this.backend.offlineMode = offlineMode;
|
this.backend.unmanaged = unmanaged;
|
||||||
this.backend.publishEnabled = enableLogs;
|
this.backend.publishEnabled = enableLogs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ export class BalenaLogBackend extends LogBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public log(message: LogMessage) {
|
public log(message: LogMessage) {
|
||||||
if (this.offlineMode || !this.publishEnabled) {
|
if (this.unmanaged || !this.publishEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export type LogMessage = Dictionary<any>;
|
export type LogMessage = Dictionary<any>;
|
||||||
|
|
||||||
export abstract class LogBackend {
|
export abstract class LogBackend {
|
||||||
public offlineMode: boolean;
|
public unmanaged: boolean;
|
||||||
public publishEnabled: boolean = true;
|
public publishEnabled: boolean = true;
|
||||||
|
|
||||||
public abstract log(message: LogMessage): void;
|
public abstract log(message: LogMessage): void;
|
||||||
|
41
src/migrations/M00002.js
Normal file
41
src/migrations/M00002.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const configJsonPath = process.env.CONFIG_MOUNT_POINT;
|
||||||
|
|
||||||
|
const { checkTruthy } = require('../lib/validation');
|
||||||
|
|
||||||
|
exports.up = function (knex, Promise) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (!configJsonPath) {
|
||||||
|
console.log('Unable to locate config.json! Things may fail unexpectedly!');
|
||||||
|
return resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readFile(configJsonPath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
console.log('Failed to read config.json! Things may fail unexpectedly!');
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data.toString());
|
||||||
|
if (parsed.localMode != null) {
|
||||||
|
return resolve(checkTruthy(parsed.localMode));
|
||||||
|
}
|
||||||
|
return resolve(false);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to parse config.json! Things may fail unexpectedly!');
|
||||||
|
return resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then(localMode => {
|
||||||
|
// We can be sure that this does not already exist in the db because of the previous
|
||||||
|
// migration
|
||||||
|
return knex('config').insert({
|
||||||
|
key: 'localMode',
|
||||||
|
value: localMode.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex, Promise) {
|
||||||
|
return Promise.reject(new Error('Not Implemented'));
|
||||||
|
};
|
@ -11,19 +11,23 @@ authenticate = (config) ->
|
|||||||
header = req.get('Authorization') ? ''
|
header = req.get('Authorization') ? ''
|
||||||
match = header.match(/^ApiKey (\w+)$/)
|
match = header.match(/^ApiKey (\w+)$/)
|
||||||
headerKey = match?[1]
|
headerKey = match?[1]
|
||||||
config.getMany([ 'apiSecret', 'localMode' ])
|
config.getMany([ 'apiSecret', 'localMode', 'unmanaged', 'osVariant' ])
|
||||||
.then (conf) ->
|
.then (conf) ->
|
||||||
if queryKey? && bufferEq(new Buffer(queryKey), new Buffer(conf.apiSecret))
|
needsAuth = if conf.unmanaged
|
||||||
next()
|
conf.osVariant is 'prod'
|
||||||
else if headerKey? && bufferEq(new Buffer(headerKey), new Buffer(conf.apiSecret))
|
|
||||||
next()
|
|
||||||
else if checkTruthy(conf.localMode)
|
|
||||||
next()
|
|
||||||
else
|
else
|
||||||
res.sendStatus(401)
|
not conf.localMode
|
||||||
|
|
||||||
|
if needsAuth
|
||||||
|
key = queryKey ? headerKey
|
||||||
|
if bufferEq(Buffer.from(key), Buffer.from(conf.apiSecret))
|
||||||
|
next()
|
||||||
|
else
|
||||||
|
res.sendStatus(401)
|
||||||
|
else
|
||||||
|
next()
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
# This should never happen...
|
res.status(503).send("Unexpected error: #{err}")
|
||||||
res.status(503).send('Invalid API key in supervisor')
|
|
||||||
|
|
||||||
module.exports = class SupervisorAPI
|
module.exports = class SupervisorAPI
|
||||||
constructor: ({ @config, @eventTracker, @routers, @healthchecks }) ->
|
constructor: ({ @config, @eventTracker, @routers, @healthchecks }) ->
|
||||||
|
@ -17,7 +17,7 @@ startupConfigFields = [
|
|||||||
'apiEndpoint'
|
'apiEndpoint'
|
||||||
'apiSecret'
|
'apiSecret'
|
||||||
'apiTimeout'
|
'apiTimeout'
|
||||||
'offlineMode'
|
'unmanaged'
|
||||||
'deviceApiKey'
|
'deviceApiKey'
|
||||||
'mixpanelToken'
|
'mixpanelToken'
|
||||||
'mixpanelHost'
|
'mixpanelHost'
|
||||||
@ -57,7 +57,7 @@ module.exports = class Supervisor extends EventEmitter
|
|||||||
apiEndpoint: conf.apiEndpoint,
|
apiEndpoint: conf.apiEndpoint,
|
||||||
uuid: conf.uuid,
|
uuid: conf.uuid,
|
||||||
deviceApiKey: conf.deviceApiKey,
|
deviceApiKey: conf.deviceApiKey,
|
||||||
offlineMode: checkTruthy(conf.offlineMode),
|
unmanaged: checkTruthy(conf.unmanaged),
|
||||||
enableLogs: checkTruthy(conf.loggingEnabled),
|
enableLogs: checkTruthy(conf.loggingEnabled),
|
||||||
localMode: checkTruthy(conf.localMode)
|
localMode: checkTruthy(conf.localMode)
|
||||||
})
|
})
|
||||||
|
@ -21,6 +21,7 @@ mockedInitialConfig = {
|
|||||||
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||||
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||||
'RESIN_SUPERVISOR_DELTA_VERSION': '2'
|
'RESIN_SUPERVISOR_DELTA_VERSION': '2'
|
||||||
|
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||||
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||||
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||||
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||||
@ -39,6 +40,7 @@ testTarget1 = {
|
|||||||
'SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
'SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||||
'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||||
'SUPERVISOR_DELTA_VERSION': '2'
|
'SUPERVISOR_DELTA_VERSION': '2'
|
||||||
|
'SUPERVISOR_LOCAL_MODE': 'false'
|
||||||
'SUPERVISOR_LOG_CONTROL': 'true'
|
'SUPERVISOR_LOG_CONTROL': 'true'
|
||||||
'SUPERVISOR_OVERRIDE_LOCK': 'false'
|
'SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||||
'SUPERVISOR_POLL_INTERVAL': '60000'
|
'SUPERVISOR_POLL_INTERVAL': '60000'
|
||||||
@ -120,6 +122,7 @@ testTargetWithDefaults2 = {
|
|||||||
'SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
'SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||||
'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
'SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||||
'SUPERVISOR_DELTA_VERSION': '2'
|
'SUPERVISOR_DELTA_VERSION': '2'
|
||||||
|
'SUPERVISOR_LOCAL_MODE': 'false'
|
||||||
'SUPERVISOR_LOG_CONTROL': 'true'
|
'SUPERVISOR_LOG_CONTROL': 'true'
|
||||||
'SUPERVISOR_OVERRIDE_LOCK': 'false'
|
'SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||||
'SUPERVISOR_POLL_INTERVAL': '60000'
|
'SUPERVISOR_POLL_INTERVAL': '60000'
|
||||||
|
@ -23,9 +23,9 @@ describe 'EventTracker', ->
|
|||||||
EventTracker.prototype.logEvent.restore()
|
EventTracker.prototype.logEvent.restore()
|
||||||
mixpanel.init.restore()
|
mixpanel.init.restore()
|
||||||
|
|
||||||
it 'initializes in offline mode', ->
|
it 'initializes in unmanaged mode', ->
|
||||||
promise = @eventTrackerOffline.init({
|
promise = @eventTrackerOffline.init({
|
||||||
offlineMode: true
|
unmanaged: true
|
||||||
uuid: 'foobar'
|
uuid: 'foobar'
|
||||||
mixpanelHost: { host: '', path: '' }
|
mixpanelHost: { host: '', path: '' }
|
||||||
})
|
})
|
||||||
@ -33,11 +33,11 @@ describe 'EventTracker', ->
|
|||||||
.then =>
|
.then =>
|
||||||
expect(@eventTrackerOffline.client).to.be.null
|
expect(@eventTrackerOffline.client).to.be.null
|
||||||
|
|
||||||
it 'logs events in offline mode, with the correct properties', ->
|
it 'logs events in unmanaged mode, with the correct properties', ->
|
||||||
@eventTrackerOffline.track('Test event', { appId: 'someValue' })
|
@eventTrackerOffline.track('Test event', { appId: 'someValue' })
|
||||||
expect(@eventTrackerOffline.logEvent).to.be.calledWith('Event:', 'Test event', JSON.stringify({ 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', ->
|
it 'initializes a mixpanel client when not in unmanaged mode', ->
|
||||||
promise = @eventTracker.init({
|
promise = @eventTracker.init({
|
||||||
mixpanelToken: 'someToken'
|
mixpanelToken: 'someToken'
|
||||||
uuid: 'barbaz'
|
uuid: 'barbaz'
|
||||||
|
@ -135,12 +135,12 @@ describe 'APIBinder', ->
|
|||||||
@apiBinder.fetchDevice.restore()
|
@apiBinder.fetchDevice.restore()
|
||||||
balenaAPI.balenaBackend.deviceKeyHandler.restore()
|
balenaAPI.balenaBackend.deviceKeyHandler.restore()
|
||||||
|
|
||||||
describe 'offline mode', ->
|
describe 'unmanaged mode', ->
|
||||||
before ->
|
before ->
|
||||||
initModels.call(this, '/config-apibinder-offline.json')
|
initModels.call(this, '/config-apibinder-offline.json')
|
||||||
|
|
||||||
it 'does not generate a key if the device is in offline mode', ->
|
it 'does not generate a key if the device is in unmanaged mode', ->
|
||||||
@config.get('offlineMode').then (mode) =>
|
@config.get('unmanaged').then (mode) =>
|
||||||
# Ensure offline mode is set
|
# Ensure offline mode is set
|
||||||
expect(mode).to.equal(true)
|
expect(mode).to.equal(true)
|
||||||
# Check that there is no deviceApiKey
|
# Check that there is no deviceApiKey
|
||||||
@ -148,12 +148,12 @@ describe 'APIBinder', ->
|
|||||||
expect(conf['deviceApiKey']).to.be.empty
|
expect(conf['deviceApiKey']).to.be.empty
|
||||||
expect(conf['uuid']).to.not.be.undefined
|
expect(conf['uuid']).to.not.be.undefined
|
||||||
|
|
||||||
describe 'Minimal config offline mode', ->
|
describe 'Minimal config unmanaged mode', ->
|
||||||
before ->
|
before ->
|
||||||
initModels.call(this, '/config-apibinder-offline2.json')
|
initModels.call(this, '/config-apibinder-offline2.json')
|
||||||
|
|
||||||
it 'does not generate a key with the minimal config', ->
|
it 'does not generate a key with the minimal config', ->
|
||||||
@config.get('offlineMode').then (mode) =>
|
@config.get('unmanaged').then (mode) =>
|
||||||
expect(mode).to.equal(true)
|
expect(mode).to.equal(true)
|
||||||
@config.getMany([ 'deviceApiKey', 'uuid' ]).then (conf) ->
|
@config.getMany([ 'deviceApiKey', 'uuid' ]).then (conf) ->
|
||||||
expect(conf['deviceApiKey']).to.be.empty
|
expect(conf['deviceApiKey']).to.be.empty
|
||||||
|
@ -31,7 +31,7 @@ describe 'Logger', ->
|
|||||||
apiEndpoint: 'https://example.com'
|
apiEndpoint: 'https://example.com'
|
||||||
uuid: 'deadbeef'
|
uuid: 'deadbeef'
|
||||||
deviceApiKey: 'secretkey'
|
deviceApiKey: 'secretkey'
|
||||||
offlineMode: false
|
unmanaged: false
|
||||||
enableLogs: true
|
enableLogs: true
|
||||||
localMode: false
|
localMode: false
|
||||||
})
|
})
|
||||||
|
@ -1 +1,16 @@
|
|||||||
{"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}
|
{
|
||||||
|
"applicationName": "supertestrpi3",
|
||||||
|
"applicationId": 78373,
|
||||||
|
"deviceType": "raspberrypi3",
|
||||||
|
"userId": 1001,
|
||||||
|
"username": "someone",
|
||||||
|
"appUpdatePollInterval": 3000,
|
||||||
|
"listenPort": 2345,
|
||||||
|
"vpnPort": 443,
|
||||||
|
"vpnEndpoint": "vpn.resin.io",
|
||||||
|
"registryEndpoint": "registry2.resin.io",
|
||||||
|
"deltaEndpoint": "https://delta.resin.io",
|
||||||
|
"mixpanelToken": "baz",
|
||||||
|
"apiKey": "boo",
|
||||||
|
"version": "2.0.6+rev3.prod"
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user