diff --git a/src/config.coffee b/src/config.coffee index d86f4ba1..c13ad729 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -1,41 +1,338 @@ -{ checkInt, checkString } = require './lib/validation' +Promise = require 'bluebird' +_ = require 'lodash' +Lock = require 'rwlock' +deviceRegister = require 'resin-register-device' +fs = Promise.promisifyAll(require('fs')) +EventEmitter = require 'events' -dockerRoot = checkString(process.env.DOCKER_ROOT) ? '/mnt/root/var/lib/rce' -apiEndpoint = checkString(process.env.API_ENDPOINT) +{ writeAndSyncFile, writeFileAtomic } = require './lib/fs-utils' +osRelease = require './lib/os-release' +supervisorVersion = require './lib/supervisor-version' +constants = require './lib/constants' -# Defaults needed for both gosuper and node supervisor are declared in entry.sh -module.exports = - apiEndpoint: apiEndpoint - apiTimeout: checkInt(process.env.API_TIMEOUT, positive: true) ? 15 * 60 * 1000 - listenPort: checkInt(process.env.LISTEN_PORT, positive: true) ? 48484 - gosuperAddress: "http://unix:#{process.env.GOSUPER_SOCKET}:" - deltaHost: checkString(process.env.DELTA_ENDPOINT) ? 'https://delta.resin.io' - registryEndpoint: checkString(process.env.REGISTRY_ENDPOINT) ? 'registry.resin.io' - pubnub: - subscribe_key: checkString(process.env.PUBNUB_SUBSCRIBE_KEY) ? process.env.DEFAULT_PUBNUB_SUBSCRIBE_KEY - publish_key: checkString(process.env.PUBNUB_PUBLISH_KEY) ? process.env.DEFAULT_PUBNUB_PUBLISH_KEY - ssl: true - mixpanelToken: checkString(process.env.MIXPANEL_TOKEN) ? process.env.DEFAULT_MIXPANEL_TOKEN - mixpanelHost: "#{apiEndpoint}/mixpanel" - dockerSocket: process.env.DOCKER_SOCKET - supervisorImage: checkString(process.env.SUPERVISOR_IMAGE) ? 'resin/rpi-supervisor' - configMountPoint: checkString(process.env.CONFIG_MOUNT_POINT) ? '/mnt/mmcblk0p1/config.json' - ledFile: checkString(process.env.LED_FILE) ? '/sys/class/leds/led0/brightness' - bootstrapRetryDelay: checkInt(process.env.BOOTSTRAP_RETRY_DELAY_MS, positive: true) ? 30000 - restartSuccessTimeout: checkInt(process.env.RESTART_SUCCESS_TIMEOUT, positive: true) ? 60000 - appUpdatePollInterval: checkInt(process.env.APPLICATION_UPDATE_POLL_INTERVAL, positive: true) ? 60000 - successMessage: 'SUPERVISOR OK' - forceSecret: - api: checkString(process.env.RESIN_SUPERVISOR_SECRET) ? null - logsChannel: checkString(process.env.RESIN_SUPERVISOR_LOGS_CHANNEL) ? null - vpnStatusPath: checkString(process.env.VPN_STATUS_PATH) ? '/mnt/root/run/openvpn/vpn_status' - hostOSVersionPath: checkString(process.env.HOST_OS_VERSION_PATH) ? '/mnt/root/etc/os-release' - dockerRoot: dockerRoot - btrfsRoot: checkString(process.env.BTRFS_ROOT) ? "#{dockerRoot}/btrfs/subvolumes" - privateAppEnvVars: [ - 'RESIN_SUPERVISOR_API_KEY' - 'RESIN_API_KEY' - ] - dataPath: checkString(process.env.RESIN_DATA_PATH) ? '/resin-data' - bootMountPoint: checkString(process.env.BOOT_MOUNTPOINT) ? '/boot' - proxyvisorHookReceiver: checkString(process.env.RESIN_PROXYVISOR_HOOK_RECEIVER) ? 'http://0.0.0.0:1337' +module.exports = class Config extends EventEmitter + constructor: ({ @db, @configPath }) -> + # These are values that come from env vars or hardcoded defaults and can be resolved synchronously + # Defaults needed for both gosuper and node supervisor are declared in entry.sh + @constants = constants + + @funcs = + version: -> + Promise.resolve(supervisorVersion) + currentApiKey: => + @getMany([ 'apiKey', 'deviceApiKey' ]) + .then ({ apiKey, deviceApiKey }) -> + return apiKey ? deviceApiKey + offlineMode: => + @getMany([ 'resinApiEndpoint', 'supervisorOfflineMode' ]) + .then ({ resinApiEndpoint, supervisorOfflineMode }) -> + return Boolean(supervisorOfflineMode) or !Boolean(resinApiEndpoint) + pubnub: => + @getMany([ 'pubnubSubscribeKey', 'pubnubPublishKey' ]) + .then ({ pubnubSubscribeKey, pubnubPublishKey }) -> + pubnub = { + subscribe_key: pubnubSubscribeKey + publish_key: pubnubPublishKey + ssl: true + } + return pubnub + resinApiEndpoint: => + # Fall back to checking if an API endpoint was passed via env vars if there's none in config.json (legacy) + @get('apiEndpoint') + .then (apiEndpoint) -> + return apiEndpoint ? @constants.apiEndpointFromEnv + + provisioned: => + @getMany([ 'uuid', 'resinApiEndpoint', 'registered_at', 'deviceId' ]) + .then (requiredValues) -> + return _.every(_.values(requiredValues), Boolean) + + osVersion: => + osRelease.getOSVersion(@constants.hostOSVersionPath) + + osVariant: => + osRelease.getOSVariant(@constants.hostOSVersionPath) + + provisioningOptions: => + @getMany([ + 'uuid' + 'userId' + 'applicationId' + 'apiKey' + 'deviceApiKey' + 'deviceType' + 'resinApiEndpoint' + 'apiTimeout' + ]).then (conf) -> + return { + uuid: conf.uuid + applicationId: conf.applicationId + userId: conf.userId + deviceType: conf.deviceType + provisioningApiKey: conf.apiKey + deviceApiKey: conf.deviceApiKey + apiEndpoint: conf.resinApiEndpoint + apiTimeout: conf.apiTimeout + } + + mixpanelHost: => + @get('resinApiEndpoint') + .then (apiEndpoint) -> + return apiEndpoint + '/mixpanel' + + extendedEnvOptions: => + @getMany([ 'uuid', 'listenPort', 'name', 'apiSecret', 'deviceApiKey', 'version', 'deviceType', 'osVersion' ]) + + fetchOptions: => + @getMany([ + 'uuid' + 'currentApiKey' + 'resinApiEndpoint' + 'deltaEndpoint' + 'delta' + 'deltaRequestTimeout' + 'deltaApplyTimeout' + 'deltaRetryCount' + 'deltaRetryInterval' + ]) + + @schema = { + apiEndpoint: { source: 'config.json' } + apiTimeout: { source: 'config.json', default: 15 * 60 * 1000 } + listenPort: { source: 'config.json', default: 48484 } + deltaEndpoint: { source: 'config.json', default: 'https://delta.resin.io' } + uuid: { source: 'config.json', mutable: true } + apiKey: { source: 'config.json', mutable: true, removeIfNull: true } + deviceApiKey: { source: 'config.json', mutable: true } + deviceType: { source: 'config.json', default: 'raspberry-pi' } + username: { source: 'config.json' } + userId: { source: 'config.json' } + deviceId: { source: 'config.json', mutable: true } + registered_at: { source: 'config.json', mutable: true } + applicationId: { source: 'config.json' } + 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 } + bootstrapRetryDelay: { source: 'config.json', default: 30000 } + supervisorOfflineMode: { source: 'config.json', default: false } + + version: { source: 'func' } + currentApiKey: { source: 'func' } + offlineMode: { source: 'func' } + pubnub: { source: 'func' } + resinApiEndpoint: { source: 'func' } + provisioned: { source: 'func' } + osVersion: { source: 'func' } + osVariant: { source: 'func' } + provisioningOptions: { source: 'func' } + mixpanelHost: { source: 'func' } + extendedEnvOptions: { source: 'func' } + fetchOptions: { source: 'func' } + + apiSecret: { source: 'db', mutable: true } + logsChannelSecret: { source: 'db', mutable: true } + name: { source: 'db', mutable: true } + initialConfigReported: { source: 'db', mutable: true, default: 'false' } + initialConfigSaved: { source: 'db', mutable: true, default: 'false' } + containersNormalised: { source: 'db', mutable: true, default: 'false' } + localMode: { source: 'db', mutable: true, default: 'false' } + loggingEnabled: { source: 'db', mutable: true, default: 'true' } + connectivityCheckEnabled: { source: 'db', mutable: true, default: 'true' } + delta: { source: 'db', mutable: true, default: 'false' } + deltaRequestTimeout: { source: 'db', mutable: true, default: '' } + deltaApplyTimeout: { source: 'db', mutable: true, default: '' } + deltaRetryCount: { source: 'db', mutable: true, default: '' } + deltaRetryInterval: { source: 'db', mutable: true, default: '' } + lockOverride: { source: 'db', mutable: true, default: 'false' } + } + + @configJsonCache = {} + + @_lock = new Lock() + @_writeLock = Promise.promisify(@_lock.async.writeLock) + @writeLockConfigJson = => + @_writeLock('config.json') + .disposer (release) -> + release() + + @_readLock = Promise.promisify(@_lock.async.readLock) + @readLockConfigJson = => + @_readLock('config.json') + .disposer (release) -> + release() + + writeConfigJson: => + atomicWritePossible = true + @configJsonPathOnHost() + .catch (err) => + console.error(err.message) + atomicWritePossible = false + return @constants.configJsonNonAtomicPath + .then (path) => + if atomicWritePossible + writeFileAtomic(path, JSON.stringify(@configJsonCache)) + else + writeAndSyncFile(path, JSON.stringify(@configJsonCache)) + + + configJsonSet: (keyVals) => + changed = false + Promise.using @writeLockConfigJson(), => + Promise.mapSeries _.keys(keyVals), (key) => + value = keyVals[key] + if @configJsonCache[key] != value + @configJsonCache[key] = value + delete @configJsonCache[key] if !value? and @schema[key].removeIfNull + changed = true + .then => + @writeConfigJson() if changed + + configJsonRemove: (key) => + changed = false + Promise.using @writeLockConfigJson(), => + Promise.try => + if @configJsonCache[key]? + delete @configJsonCache[key] + changed = true + .then => + @writeConfigJson() if changed + + configJsonPathOnHost: => + Promise.try => + return @configPath if @configPath? + return @constants.configJsonPathOnHost if @constants.configJsonPathOnHost? + osRelease.getOSVersion(@constants.hostOSVersionPath) + .then (osVersion) => + if /^Resin OS 2./.test(osVersion) + return "#{@constants.bootMountPointFromEnv}/config.json" + else if /^Resin OS 1./.test(osVersion) + # In Resin OS 1.12, $BOOT_MOUNTPOINT was added and it coincides with config.json's path + return "#{@constants.bootMountPointFromEnv}/config.json" if @constants.bootMountPointFromEnv + # Older 1.X versions have config.json here + return '/mnt/conf/config.json' + else + # In non-resinOS hosts (or older than 1.0.0), if CONFIG_JSON_PATH wasn't passed then we can't do atomic changes + # (only access to config.json we have is in /boot, which is assumed to be a file bind mount where rename is impossible) + throw new Error('Could not determine config.json path on host, atomic write will not be possible') + .then (path) => + return "#{@constants.rootMountPoint}#{path}" + + configJsonPath: => + @configJsonPathOnHost() + .catch (err) => + console.error(err.message) + return @constants.configJsonNonAtomicPath + + readConfigJson: => + @configJsonPath() + .then (path) -> + fs.readFileAsync(path) + .then(JSON.parse) + + newUniqueKey: -> + deviceRegister.generateUniqueKey() + + generateRequiredFields: => + @getMany([ 'uuid', 'deviceApiKey', 'apiSecret', 'logsChannelSecret' ]) + .then ({ uuid, deviceApiKey, apiSecret, logsChannelSecret }) => + if !uuid? or !deviceApiKey? or !apiSecret? or !logsChannelSecret? + uuid ?= @newUniqueKey() + deviceApiKey ?= @newUniqueKey() + apiSecret ?= @newUniqueKey() + logsChannelSecret ?= @newUniqueKey() + @set({ uuid, deviceApiKey, apiSecret, logsChannelSecret }) + + regenerateRegistrationFields: => + uuid = deviceRegister.generateUniqueKey() + deviceApiKey = deviceRegister.generateUniqueKey() + @set({ uuid, deviceApiKey }) + + get: (key, trx) => + db = trx ? @db.models + # Get value for "key" from config.json or db + Promise.try => + switch @schema[key]?.source + when undefined + throw new Error("Unknown config value #{key}") + when 'func' + @funcs[key]() + .catch (err) -> + console.error("Error getting config value for #{key}", err, err.stack) + return null + when 'config.json' + Promise.using @readLockConfigJson(), => + return @configJsonCache[key] + when 'db' + db('config').select('value').where({ key }) + .then ([ conf ]) -> + return conf?.value + .then (value) => + if !value? and @schema[key]?.default? + return @schema[key].default + return value + + getMany: (keys, trx) => + # Get the values for several keys in an array + Promise.all(_.map(keys, (key) => @get(key, trx) )) + .then (values) -> + out = {} + for key, i in keys + out[key] = values[i] + return out + + # Sets config values as atomically as possible + # Is atomic if all values have the same source, otherwise it's atomic for each source + set: (keyValues, trx) => + Promise.try => + # Write value to config.json or DB + { configJsonVals, dbVals } = _.reduce(keyValues, (acc, val, key) => + throw new Error("Attempt to change immutable config value #{key}") if !@schema[key]?.mutable + switch @schema[key]?.source + when 'config.json' + acc.configJsonVals[key] = val + when 'db' + acc.dbVals[key] = val + return acc + , { configJsonVals: {}, dbVals: {} }) + + setValuesInTransaction = (tx) => + dbKeys = _.keys(dbVals) + @getMany(dbKeys, tx) + .then (oldValues) => + Promise.map dbKeys, (key) => + value = dbVals[key] + if oldValues[key] != value + @db.upsertModel('config', { key, value }, { key }, tx) + .then => + @configJsonSet(configJsonVals) if !_.isEmpty(configJsonVals) + if trx? + setValuesInTransaction(trx) + else + @db.transaction (tx) -> + setValuesInTransaction(tx) + .then => + setImmediate => + @emit('change', keyValues) + + # Clear a value from config.json or DB + # (will be used to clear the provisioning key) + # only mutable fields! + remove: (key) => + Promise.try => + throw new Error("Attempt to delete immutable config value #{key}") if !@schema[key]?.mutable + switch @schema[key]?.source + when 'config.json' + @configJsonRemove(key) + when 'db' + @db.models('config').del().where({ key }) + + init: => + # Read config.json and cache its values + # get or generate apiSecret, logsChannelSecret, uuid + @readConfigJson() + .then (configJson) => + _.assign(@configJsonCache, configJson) + .then => + # get or generate uuid, apiSecret, logsChannelSecret + @generateRequiredFields() diff --git a/src/db.coffee b/src/db.coffee index 342ac82e..8e2ec03f 100644 --- a/src/db.coffee +++ b/src/db.coffee @@ -1,113 +1,228 @@ Promise = require 'bluebird' Knex = require 'knex' -knex = Knex( - client: 'sqlite3' - connection: - filename: '/data/database.sqlite' - useNullAsDefault: true -) +constants = require './lib/constants' -addColumn = (table, column, type) -> - knex.schema.hasColumn(table, column) - .then (exists) -> - if not exists - knex.schema.table table, (t) -> - t[type](column) +module.exports = class DB + constructor: ({ @databasePath } = {}) -> + @databasePath ?= constants.databasePath + @knex = Knex( + client: 'sqlite3' + connection: + filename: @databasePath + useNullAsDefault: true + ) -knex.init = Promise.all([ - knex.schema.hasTable('config') - .then (exists) -> - if not exists - knex.schema.createTable 'config', (t) -> - t.string('key').primary() - t.string('value') + addColumn: (table, column, type) => + @knex.schema.hasColumn(table, column) + .then (exists) => + if not exists + @knex.schema.table table, (t) -> + t[type](column) - knex.schema.hasTable('deviceConfig') - .then (exists) -> - if not exists - knex.schema.createTable 'deviceConfig', (t) -> - t.json('values') - t.json('targetValues') - .then -> - knex('deviceConfig').select() - .then (deviceConfigs) -> - knex('deviceConfig').insert({ values: '{}', targetValues: '{}' }) if deviceConfigs.length == 0 + dropColumn: (table, column) => + @knex.schema.hasColumn(table, column) + .then (exists) => + if exists + @knex.schema.table table, (t) -> + t.dropColumn(column) - knex.schema.hasTable('app') - .then (exists) -> - if not exists - knex.schema.createTable 'app', (t) -> - t.increments('id').primary() - t.string('name') - t.string('containerName') - t.string('commit') - t.string('imageId') - t.string('appId') - t.boolean('privileged') - t.json('env') - t.json('config') - t.boolean('markedForDeletion') - else - Promise.all [ - addColumn('app', 'commit', 'string') - addColumn('app', 'appId', 'string') - addColumn('app', 'containerName', 'string') - addColumn('app', 'config', 'json') - addColumn('app', 'markedForDeletion', 'boolean') - ] - .then -> - # When updating from older supervisors, config can be null - knex('app').update({ config: '{}' }).whereNull('config') - .then -> - knex('app').update({ markedForDeletion: false }).whereNull('markedForDeletion') + dropTableIfExists: (tableName, trx) => + knex = trx ? @knex + knex.schema.hasTable(tableName) + .then (exists) -> + knex.schema.dropTable(tableName) if exists - knex.schema.hasTable('dependentApp') - .then (exists) -> - if not exists - knex.schema.createTable 'dependentApp', (t) -> - t.increments('id').primary() - t.string('appId') - t.string('parentAppId') - t.string('name') - t.string('commit') - t.string('imageId') - t.json('config') - t.json('environment') - else - addColumn('dependentApp', 'environment', 'json') + _migrateToV2: => + # Drop all tables, but keep the info we need + @transaction (trx) => + trx.schema.hasTable('legacyData') + .then (exists) => + if not exists + trx.schema.createTable 'legacyData', (t) -> + t.json('apps') + t.json('dependentApps') + t.json('dependentDevices') + .then => + Promise.join( + trx.schema.hasTable('app') + .then (exists) -> + if exists + trx.select().from('app') + else + return [] + .then(JSON.stringify) + trx.schema.hasTable('dependentDevice') + .then (exists) -> + if exists + trx.select().from('dependentDevice') + else + return [] + .then(JSON.stringify) + trx.schema.hasTable('dependentApp') + .then (exists) -> + if exists + trx.select().from('dependentApp') + else + return [] + .then(JSON.stringify) + (apps, dependentDevices, dependentApps) => + @upsertModel('legacyData', { apps, dependentDevices, dependentApps }, {}, trx) + ) + .then => + @dropTableIfExists('app', trx) + .then => + @dropTableIfExists('deviceConfig', trx) + .then => + @dropTableIfExists('dependentApp', trx) + .then => + @dropTableIfExists('dependentDevice', trx) + .then => + @dropTableIfExists('image', trx) + .then => + @dropTableIfExists('container', trx) - knex.schema.hasTable('dependentDevice') - .then (exists) -> - if not exists - knex.schema.createTable 'dependentDevice', (t) -> - t.increments('id').primary() - t.string('uuid') - t.string('appId') - t.string('localId') - t.string('device_type') - t.string('logs_channel') - t.string('deviceId') - t.boolean('is_online') - t.string('name') - t.string('status') - t.string('download_progress') - t.string('is_managed_by') - t.dateTime('lock_expiry_date') - t.string('commit') - t.string('targetCommit') - t.json('environment') - t.json('targetEnvironment') - t.json('config') - t.json('targetConfig') - t.boolean('markedForDeletion') - else - Promise.all [ - addColumn('dependentDevice', 'markedForDeletion', 'boolean') - addColumn('dependentDevice', 'localId', 'string') - addColumn('dependentDevice', 'is_managed_by', 'string') - addColumn('dependentDevice', 'lock_expiry_date', 'dateTime') - ] -]) + finishMigration: => + @transaction (trx) => + @upsertModel('config', { key: 'schema-version', value: '2' }, { key: 'schema-version' }, trx) + .then => + @dropTableIfExists('legacyData', trx) -module.exports = knex + _initConfigAndGetSchemaVersion: => + @knex.schema.hasTable('config') + .then (exists) => + if not exists + @knex.schema.createTable 'config', (t) -> + t.string('key').primary() + t.string('value') + .then => + @knex('config').insert({ key: 'schema-version', value: '2' }) + .then => + @knex('config').where({ key: 'schema-version' }).select() + .then ([ schemaVersion ]) -> + return schemaVersion + + init: => + migrationNeeded = false + @_initConfigAndGetSchemaVersion() + .then (schemaVersion) => + if !schemaVersion? or schemaVersion.value != '2' + # We're on an old db, need to migrate + migrationNeeded = true + @_migrateToV2() + .then => + Promise.all([ + @knex.schema.hasTable('deviceConfig') + .then (exists) => + if not exists + @knex.schema.createTable 'deviceConfig', (t) -> + t.json('targetValues') + .then => + @knex('deviceConfig').select() + .then (deviceConfigs) => + @knex('deviceConfig').insert({ targetValues: '{}' }) if deviceConfigs.length == 0 + + @knex.schema.hasTable('app') + .then (exists) => + if not exists + @knex.schema.createTable 'app', (t) -> + t.increments('id').primary() + t.string('name') + t.string('releaseId') + t.string('commit') + t.string('appId') + t.json('services') + t.json('networks') + t.json('volumes') + + @knex.schema.hasTable('dependentAppTarget') + .then (exists) => + if not exists + @knex.schema.createTable 'dependentAppTarget', (t) -> + t.increments('id').primary() + t.string('appId') + t.string('parentApp') + t.string('name') + t.string('commit') + t.string('releaseId') + t.string('imageId') + t.string('image') + t.json('environment') + t.json('config') + + @knex.schema.hasTable('dependentDeviceTarget') + .then (exists) => + if not exists + @knex.schema.createTable 'dependentDeviceTarget', (t) -> + t.increments('id').primary() + t.string('uuid') + t.string('name') + t.json('apps') + + @knex.schema.hasTable('dependentApp') + .then (exists) => + if not exists + @knex.schema.createTable 'dependentApp', (t) -> + t.increments('id').primary() + t.string('appId') + t.string('parentApp') + t.string('name') + t.string('commit') + t.string('releaseId') + t.string('image') + t.json('environment') + t.json('config') + + @knex.schema.hasTable('dependentDevice') + .then (exists) => + if not exists + @knex.schema.createTable 'dependentDevice', (t) -> + t.increments('id').primary() + t.string('uuid') + t.string('appId') + t.string('localId') + t.string('device_type') + t.string('logs_channel') + t.string('deviceId') + t.boolean('is_online') + t.string('name') + t.string('status') + t.string('download_progress') + t.string('is_managed_by') + t.dateTime('lock_expiry_date') + t.string('commit') + t.string('targetCommit') + t.json('environment') + t.json('targetEnvironment') + t.json('config') + t.json('targetConfig') + t.boolean('markedForDeletion') + + @knex.schema.hasTable('image') + .then (exists) => + if not exists + @knex.schema.createTable 'image', (t) -> + t.increments('id').primary() + t.string('name') + t.string('appId') + t.string('serviceId') + t.string('serviceName') + t.string('imageId') + t.string('releaseId') + t.boolean('dependent') + ]) + .then -> + return migrationNeeded + + # Returns a knex object for one of the models (tables) + models: (modelName) => + @knex(modelName) + + upsertModel: (modelName, obj, id, trx) => + knex = trx ? @knex + knex(modelName).update(obj).where(id) + .then (n) -> + knex(modelName).insert(obj) if n == 0 + + transaction: (cb) => + @knex.transaction(cb) diff --git a/src/lib/constants.coffee b/src/lib/constants.coffee new file mode 100644 index 00000000..8ec0d8f5 --- /dev/null +++ b/src/lib/constants.coffee @@ -0,0 +1,34 @@ +{ checkString } = require './validation' + +bootMountPointFromEnv = checkString(process.env.BOOT_MOUNTPOINT) +rootMountPoint = checkString(process.env.ROOT_MOUNTPOINT) ? '/mnt/root' + +module.exports = + rootMountPoint: rootMountPoint + databasePath: checkString(process.env.DATABASE_PATH) ? '/data/database.sqlite' + gosuperAddress: "http://unix:#{process.env.GOSUPER_SOCKET}:" + dockerSocket: process.env.DOCKER_SOCKET ? '/var/run/docker.sock' + supervisorImage: checkString(process.env.SUPERVISOR_IMAGE) ? 'resin/rpi-supervisor' + ledFile: checkString(process.env.LED_FILE) ? '/sys/class/leds/led0/brightness' + forceSecret: # Only used for development + api: checkString(process.env.RESIN_SUPERVISOR_SECRET) ? null + logsChannel: checkString(process.env.RESIN_SUPERVISOR_LOGS_CHANNEL) ? null + vpnStatusPath: checkString(process.env.VPN_STATUS_PATH) ? "#{rootMountPoint}/run/openvpn/vpn_status" + hostOSVersionPath: checkString(process.env.HOST_OS_VERSION_PATH) ? "#{rootMountPoint}/etc/os-release" + privateAppEnvVars: [ + 'RESIN_SUPERVISOR_API_KEY' + 'RESIN_API_KEY' + ] + dataPath: checkString(process.env.RESIN_DATA_PATH) ? '/resin-data' + bootMountPointFromEnv: bootMountPointFromEnv + bootMountPoint: bootMountPointFromEnv ? '/boot' + configJsonPathOnHost: checkString(process.env.CONFIG_JSON_PATH) + proxyvisorHookReceiver: checkString(process.env.RESIN_PROXYVISOR_HOOK_RECEIVER) ? 'http://0.0.0.0:1337' + apiEndpointFromEnv: checkString(process.env.API_ENDPOINT) + configJsonNonAtomicPath: '/boot/config.json' + defaultPubnubSubscribeKey: process.env.DEFAULT_PUBNUB_SUBSCRIBE_KEY + defaultPubnubPublishKey: process.env.DEFAULT_PUBNUB_PUBLISH_KEY + defaultMixpanelToken: process.env.DEFAULT_MIXPANEL_TOKEN + allowedInterfaces: ['resin-vpn', 'tun0', 'docker0', 'lo'] + appsJsonPath: process.env.APPS_JSON_PATH ? '/boot/apps.json' + ipAddressUpdateInterval: 30000 diff --git a/src/lib/os-release.coffee b/src/lib/os-release.coffee index 7821033e..17a84655 100644 --- a/src/lib/os-release.coffee +++ b/src/lib/os-release.coffee @@ -10,10 +10,12 @@ exports.getOSReleaseField = (path, field) -> for line in lines [ key, val ] = line.split('=') releaseItems[_.trim(key)] = _.trim(val) + if !releaseItems[field]? + throw new Error("Field #{field} not available in #{path}") # Remove enclosing quotes: http://stackoverflow.com/a/19156197/2549019 return releaseItems[field].replace(/^"(.+(?="$))"$/, '$1') catch err - console.log('Could not get OS release field: ', err, err.stack) + console.log('Could not get OS release field: ', err.message) return undefined