Replace io.resin labels (and their env vars) with io.balena equivalents

But we keep backwards compatibility by normalizing existing io.resin labels
into io.balena ones, and adding both RESIN_ and BALENA_ env vars for these features.

Change-Type: minor
Signed-off-by: Pablo Carranza Velez <pablo@balena.io>
This commit is contained in:
Pablo Carranza Velez 2018-10-17 16:34:07 +02:00
parent 3b7bf9a4b7
commit ed3f5522ae
10 changed files with 110 additions and 55 deletions

View File

@ -96,7 +96,7 @@ module.exports = class ApplicationManager extends EventEmitter
# Only called for dead containers, so no need to take locks or anything # Only called for dead containers, so no need to take locks or anything
@services.remove(step.current) @services.remove(step.current)
updateMetadata: (step, { force = false, skipLock = false } = {}) => updateMetadata: (step, { force = false, skipLock = false } = {}) =>
skipLock or= checkTruthy(step.current.config.labels['io.resin.legacy-container']) skipLock or= checkTruthy(step.current.config.labels['io.balena.legacy-container'])
@_lockingIfNecessary step.current.appId, { force, skipLock: skipLock or step.options?.skipLock }, => @_lockingIfNecessary step.current.appId, { force, skipLock: skipLock or step.options?.skipLock }, =>
@services.updateMetadata(step.current, step.target) @services.updateMetadata(step.current, step.target)
restart: (step, { force = false, skipLock = false } = {}) => restart: (step, { force = false, skipLock = false } = {}) =>
@ -591,11 +591,11 @@ module.exports = class ApplicationManager extends EventEmitter
# Either this is a new service, or the current one has already been killed # Either this is a new service, or the current one has already been killed
return @_fetchOrStartStep(current, target, needsDownload, dependenciesMetForStart) return @_fetchOrStartStep(current, target, needsDownload, dependenciesMetForStart)
else else
strategy = checkString(target.config.labels['io.resin.update.strategy']) strategy = checkString(target.config.labels['io.balena.update.strategy'])
validStrategies = [ 'download-then-kill', 'kill-then-download', 'delete-then-download', 'hand-over' ] validStrategies = [ 'download-then-kill', 'kill-then-download', 'delete-then-download', 'hand-over' ]
if !_.includes(validStrategies, strategy) if !_.includes(validStrategies, strategy)
strategy = 'download-then-kill' strategy = 'download-then-kill'
timeout = checkInt(target.config.labels['io.resin.update.handover-timeout']) timeout = checkInt(target.config.labels['io.balena.update.handover-timeout'])
return @_strategySteps[strategy](current, target, needsDownload, dependenciesMetForStart, dependenciesMetForKill, needsSpecialKill, timeout) return @_strategySteps[strategy](current, target, needsDownload, dependenciesMetForStart, dependenciesMetForKill, needsSpecialKill, timeout)
_nextStepsForAppUpdate: (currentApp, targetApp, localMode, availableImages = [], downloading = []) => _nextStepsForAppUpdate: (currentApp, targetApp, localMode, availableImages = [], downloading = []) =>
@ -608,12 +608,12 @@ module.exports = class ApplicationManager extends EventEmitter
currentApp ?= emptyApp currentApp ?= emptyApp
if currentApp.services?.length == 1 and targetApp.services?.length == 1 and if currentApp.services?.length == 1 and targetApp.services?.length == 1 and
targetApp.services[0].serviceName == currentApp.services[0].serviceName and targetApp.services[0].serviceName == currentApp.services[0].serviceName and
checkTruthy(currentApp.services[0].config.labels['io.resin.legacy-container']) checkTruthy(currentApp.services[0].config.labels['io.balena.legacy-container'])
# This is a legacy preloaded app or container, so we didn't have things like serviceId. # This is a legacy preloaded app or container, so we didn't have things like serviceId.
# We hack a few things to avoid an unnecessary restart of the preloaded app # We hack a few things to avoid an unnecessary restart of the preloaded app
# (but ensuring it gets updated if it actually changed) # (but ensuring it gets updated if it actually changed)
targetApp.services[0].config.labels['io.resin.legacy-container'] = currentApp.services[0].labels['io.resin.legacy-container'] targetApp.services[0].config.labels['io.balena.legacy-container'] = currentApp.services[0].labels['io.balena.legacy-container']
targetApp.services[0].config.labels['io.resin.service-id'] = currentApp.services[0].labels['io.resin.service-id'] targetApp.services[0].config.labels['io.balena.service-id'] = currentApp.services[0].labels['io.balena.service-id']
targetApp.services[0].serviceId = currentApp.services[0].serviceId targetApp.services[0].serviceId = currentApp.services[0].serviceId
appId = targetApp.appId ? currentApp.appId appId = targetApp.appId ? currentApp.appId

View File

@ -313,4 +313,9 @@ module.exports = class Images extends EventEmitter
isSameImage: @isSameImage isSameImage: @isSameImage
_getLocalModeImages: => _getLocalModeImages: =>
@docker.listImages(filters: label: [ 'io.resin.local.image=1' ]) Promise.join(
@docker.listImages(filters: label: [ 'io.resin.local.image=1' ])
@docker.listImages(filters: label: [ 'io.balena.local.image=1' ])
(legacyImages, currentImages) ->
_.unionBy(legacyImages, currentImages, 'Id')
)

View File

@ -1,5 +1,6 @@
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { fs } from 'mz'; import { fs } from 'mz';
import * as _ from 'lodash';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import Docker = require('../lib/docker-utils'); import Docker = require('../lib/docker-utils');
@ -17,11 +18,7 @@ export class NetworkManager {
} }
public getAll(): Bluebird<Network[]> { public getAll(): Bluebird<Network[]> {
return Bluebird.resolve(this.docker.listNetworks({ return this.getWithBothLabels()
filters: {
label: [ 'io.resin.supervised' ],
},
}))
.map((network: { Name: string }) => { .map((network: { Name: string }) => {
return this.docker.getNetwork(network.Name).inspect() return this.docker.getNetwork(network.Name).inspect()
.then((net) => { .then((net) => {
@ -90,4 +87,22 @@ export class NetworkManager {
}); });
} }
private getWithBothLabels() {
return Bluebird.join(
this.docker.listNetworks({
filters: {
label: [ 'io.resin.supervised' ],
},
}),
this.docker.listNetworks({
filters: {
label: [ 'io.balena.supervised' ],
},
}),
(legacyNetworks, currentNetworks) => {
return _.unionBy(currentNetworks, legacyNetworks, 'Id');
},
);
}
} }

View File

@ -9,6 +9,7 @@ import {
import logTypes = require('../lib/log-types'); import logTypes = require('../lib/log-types');
import { checkInt } from '../lib/validation'; import { checkInt } from '../lib/validation';
import { Logger } from '../logger'; import { Logger } from '../logger';
import * as ComposeUtils from './utils';
import { import {
DockerIPAMConfig, DockerIPAMConfig,
@ -83,7 +84,7 @@ export class Network {
}, },
enableIPv6: network.EnableIPv6, enableIPv6: network.EnableIPv6,
internal: network.Internal, internal: network.Internal,
labels: _.omit(network.Labels, [ 'io.resin.supervised' ]), labels: _.omit(ComposeUtils.normalizeLabels(network.Labels), [ 'io.balena.supervised' ]),
options: network.Options, options: network.Options,
}; };
@ -125,6 +126,7 @@ export class Network {
labels: { }, labels: { },
options: { }, options: { },
}); });
net.config.labels = ComposeUtils.normalizeLabels(net.config.labels);
return net; return net;
} }
@ -177,7 +179,7 @@ export class Network {
EnableIPv6: this.config.enableIPv6, EnableIPv6: this.config.enableIPv6,
Internal: this.config.internal, Internal: this.config.internal,
Labels: _.merge({}, { Labels: _.merge({}, {
'io.resin.supervised': 'true', 'io.balena.supervised': 'true',
}, this.config.labels), }, this.config.labels),
}; };
} }

View File

@ -88,7 +88,7 @@ module.exports = class ServiceManager extends EventEmitter
@logger.logSystemEvent(logTypes.removeDeadServiceError, { service, error: err }) @logger.logSystemEvent(logTypes.removeDeadServiceError, { service, error: err })
getAllByAppId: (appId) => getAllByAppId: (appId) =>
@getAll("io.resin.app-id=#{appId}") @getAll("app-id=#{appId}")
stopAllByAppId: (appId) => stopAllByAppId: (appId) =>
Promise.map @getAllByAppId(appId), (service) => Promise.map @getAllByAppId(appId), (service) =>
@ -174,10 +174,20 @@ module.exports = class ServiceManager extends EventEmitter
.finally => .finally =>
@reportChange(containerId) @reportChange(containerId)
_listWithBothLabels: (labelList) =>
listWithPrefix = (prefix) =>
@docker.listContainers({ all: true, filters: label: _.map(labelList, (v) -> prefix + v) })
Promise.join(
listWithPrefix('io.resin.')
listWithPrefix('io.balena.')
(legacyContainers, currentContainers) ->
_.unionBy(legacyContainers, currentContainers, 'Id')
)
# Gets all existing containers that correspond to apps # Gets all existing containers that correspond to apps
getAll: (extraLabelFilters = []) => getAll: (extraLabelFilters = []) =>
filters = label: [ 'io.resin.supervised' ].concat(extraLabelFilters) filterLabels = [ 'supervised' ].concat(extraLabelFilters)
@docker.listContainers({ all: true, filters }) @_listWithBothLabels(filterLabels)
.mapSeries (container) => .mapSeries (container) =>
@docker.getContainer(container.Id).inspect() @docker.getContainer(container.Id).inspect()
.then(Service.fromDockerContainer) .then(Service.fromDockerContainer)
@ -190,7 +200,7 @@ module.exports = class ServiceManager extends EventEmitter
# Returns the first container matching a service definition # Returns the first container matching a service definition
get: (service) => get: (service) =>
@getAll("io.resin.service-id=#{service.serviceId}") @getAll("service-id=#{service.serviceId}")
.filter((currentService) -> currentService.isEqualConfig(service)) .filter((currentService) -> currentService.isEqualConfig(service))
.then (services) -> .then (services) ->
if services.length == 0 if services.length == 0
@ -218,7 +228,7 @@ module.exports = class ServiceManager extends EventEmitter
getByDockerContainerId: (containerId) => getByDockerContainerId: (containerId) =>
@docker.getContainer(containerId).inspect() @docker.getContainer(containerId).inspect()
.then (container) -> .then (container) ->
if !container.Config.Labels['io.resin.supervised']? if !(container.Config.Labels['io.balena.supervised']? or container.Config.Labels['io.resin.supervised']?)
return null return null
return Service.fromDockerContainer(container) return Service.fromDockerContainer(container)
@ -267,7 +277,7 @@ module.exports = class ServiceManager extends EventEmitter
.then => .then =>
@start(targetService) @start(targetService)
.then => .then =>
@waitToKill(currentService, targetService.config.labels['io.resin.update.handover-timeout']) @waitToKill(currentService, targetService.config.labels['io.balena.update.handover-timeout'])
.then => .then =>
@kill(currentService) @kill(currentService)

View File

@ -201,13 +201,13 @@ export class Service {
service.appId || 0, service.appId || 0,
service.serviceName || '', service.serviceName || '',
); );
config.labels = Service.extendLabels( config.labels = ComposeUtils.normalizeLabels(Service.extendLabels(
config.labels || { }, config.labels || { },
options, options,
service.appId || 0, service.appId || 0,
service.serviceId || 0, service.serviceId || 0,
service.serviceName || '', service.serviceName || '',
); ));
// Any other special case handling // Any other special case handling
if (config.networkMode === 'host' && !config.hostname) { if (config.networkMode === 'host' && !config.hostname) {
@ -427,7 +427,7 @@ export class Service {
image: container.Config.Image, image: container.Config.Image,
environment: conversions.envArrayToObject(container.Config.Env || [ ]), environment: conversions.envArrayToObject(container.Config.Env || [ ]),
privileged: container.HostConfig.Privileged || false, privileged: container.HostConfig.Privileged || false,
labels: container.Config.Labels || { }, labels: ComposeUtils.normalizeLabels(container.Config.Labels || { }),
running: container.State.Running, running: container.State.Running,
restart, restart,
capAdd: container.HostConfig.CapAdd || [ ], capAdd: container.HostConfig.CapAdd || [ ],
@ -471,9 +471,9 @@ export class Service {
tty: container.Config.Tty || false, tty: container.Config.Tty || false,
}; };
svc.appId = checkInt(container.Config.Labels['io.resin.app-id']) || null; svc.appId = checkInt(svc.config.labels['io.balena.app-id']) || null;
svc.serviceId = checkInt(container.Config.Labels['io.resin.service-id']) || null; svc.serviceId = checkInt(svc.config.labels['io.balena.service-id']) || null;
svc.serviceName = container.Config.Labels['io.resin.service-name']; svc.serviceName = svc.config.labels['io.balena.service-name'];
const nameMatch = container.Name.match(/.*_(\d+)_(\d+)$/); const nameMatch = container.Name.match(/.*_(\d+)_(\d+)$/);
svc.imageId = nameMatch != null ? checkInt(nameMatch[1]) || null : null; svc.imageId = nameMatch != null ? checkInt(nameMatch[1]) || null : null;
@ -788,10 +788,10 @@ export class Service {
serviceName: string, serviceName: string,
): { [labelName: string]: string } { ): { [labelName: string]: string } {
let newLabels = _.defaults(labels, { let newLabels = _.defaults(labels, {
'io.resin.supervised': 'true', 'io.balena.supervised': 'true',
'io.resin.app-id': appId.toString(), 'io.balena.app-id': appId.toString(),
'io.resin.service-id': serviceId.toString(), 'io.balena.service-id': serviceId.toString(),
'io.resin.service-name': serviceName, 'io.balena.service-name': serviceName,
}); });
const imageLabels = _.get(imageInfo, 'Config.Labels', { }); const imageLabels = _.get(imageInfo, 'Config.Labels', { });

View File

@ -300,45 +300,48 @@ export function addFeaturesFromLabels(
service: Service, service: Service,
options: DeviceMetadata, options: DeviceMetadata,
): void { ): void {
if (checkTruthy(service.config.labels['io.resin.features.dbus'])) { const setEnvVariables = function (key: string, val: string) {
service.config.environment[`RESIN_${key}`] = val;
service.config.environment[`BALENA_${key}`] = val;
};
if (checkTruthy(service.config.labels['io.balena.features.dbus'])) {
service.config.volumes.push('/run/dbus:/host/run/dbus'); service.config.volumes.push('/run/dbus:/host/run/dbus');
} }
if ( if (
checkTruthy(service.config.labels['io.resin.features.kernel-modules']) && checkTruthy(service.config.labels['io.balena.features.kernel-modules']) &&
options.hostPathExists.modules options.hostPathExists.modules
) { ) {
service.config.volumes.push('/lib/modules:/lib/modules'); service.config.volumes.push('/lib/modules:/lib/modules');
} }
if ( if (
checkTruthy(service.config.labels['io.resin.features.firmware']) && checkTruthy(service.config.labels['io.balena.features.firmware']) &&
options.hostPathExists.firmware options.hostPathExists.firmware
) { ) {
service.config.volumes.push('/lib/firmware:/lib/firmware'); service.config.volumes.push('/lib/firmware:/lib/firmware');
} }
if (checkTruthy(service.config.labels['io.resin.features.balena-socket'])) { if (checkTruthy(service.config.labels['io.balena.features.balena-socket'])) {
service.config.volumes.push('/var/run/balena.sock:/var/run/balena.sock'); service.config.volumes.push('/var/run/balena.sock:/var/run/balena.sock');
if (service.config.environment['DOCKER_HOST'] == null) { if (service.config.environment['DOCKER_HOST'] == null) {
service.config.environment['DOCKER_HOST'] = 'unix:///var/run/balena.sock'; service.config.environment['DOCKER_HOST'] = 'unix:///var/run/balena.sock';
} }
} }
if (checkTruthy('io.resin.features.resin-api')) { if (checkTruthy('io.balena.features.balena-api')) {
service.config.environment['RESIN_API_KEY'] = options.deviceApiKey; setEnvVariables('API_KEY', options.deviceApiKey);
} }
if (checkTruthy(service.config.labels['io.resin.features.supervisor-api'])) { if (checkTruthy(service.config.labels['io.balena.features.supervisor-api'])) {
service.config.environment['RESIN_SUPERVISOR_PORT'] = options.listenPort.toString(); setEnvVariables('SUPERVISOR_PORT', options.listenPort.toString());
service.config.environment['RESIN_SUPERVISOR_API_KEY'] = options.apiSecret; setEnvVariables('SUPERVISOR_API_KEY', options.apiSecret);
if (service.config.networkMode === 'host') { if (service.config.networkMode === 'host') {
service.config.environment['RESIN_SUPERVISOR_HOST'] = '127.0.0.1'; setEnvVariables('SUPERVISOR_HOST', '127.0.0.1');
service.config.environment['RESIN_SUPERVISOR_ADDRESS'] = `http://127.0.0.1:${options.listenPort}`; setEnvVariables('SUPERVISOR_ADDRESS', `http://127.0.0.1:${options.listenPort}`);
} else { } else {
service.config.environment['RESIN_SUPERVISOR_HOST'] = options.supervisorApiHost; setEnvVariables('SUPERVISOR_HOST', options.supervisorApiHost);
service.config.environment['RESIN_SUPERVISOR_ADDRESS'] = setEnvVariables('SUPERVISOR_ADDRESS', `http://${options.supervisorApiHost}:${options.listenPort}`);
`http://${options.supervisorApiHost}:${options.listenPort}`;
service.config.networks[constants.supervisorNetworkInterface] = { }; service.config.networks[constants.supervisorNetworkInterface] = { };
} }
} else { } else {
@ -442,3 +445,14 @@ export function normalizeNullValues(obj: Dictionary<any>): void {
} }
}); });
} }
export function normalizeLabels(
labels: { [key: string]: string },
): { [key: string]: string } {
const legacyLabels = _.mapKeys(_.pickBy(labels, (_v, k) => _.startsWith(k, 'io.resin.')), (_v, k) => {
return k.replace(/resin/g, 'balena'); // e.g. io.resin.features.resin-api -> io.balena.features.balena-api
});
const balenaLabels = _.pickBy(labels, (_v, k) => _.startsWith(k, 'io.balena.'));
const otherLabels = _.pickBy(labels, (_v, k) => !(_.startsWith(k, 'io.balena.') || _.startsWith(k, 'io.resin.')));
return _.assign({}, otherLabels, legacyLabels, balenaLabels);
}

View File

@ -8,6 +8,7 @@ constants = require '../lib/constants'
{ NotFoundError } = require '../lib/errors' { NotFoundError } = require '../lib/errors'
{ defaultLegacyVolume } = require '../lib/migration' { defaultLegacyVolume } = require '../lib/migration'
{ safeRename } = require '../lib/fs-utils' { safeRename } = require '../lib/fs-utils'
ComposeUtils = require './utils'
module.exports = class Volumes module.exports = class Volumes
constructor: ({ @docker, @logger }) -> constructor: ({ @docker, @logger }) ->
@ -20,19 +21,27 @@ module.exports = class Volumes
name: name name: name
appId: appId appId: appId
config: { config: {
labels: _.omit(volume.Labels, _.keys(constants.defaultVolumeLabels)) labels: _.omit(ComposeUtils.normalizeLabels(volume.Labels), _.keys(constants.defaultVolumeLabels))
driverOpts: volume.Options driverOpts: volume.Options
} }
handle: volume handle: volume
} }
_listWithBothLabels: =>
Promise.join(
@docker.listVolumes(filters: label: [ 'io.resin.supervised' ])
@docker.listVolumes(filters: label: [ 'io.balena.supervised' ])
(legacyVolumesResponse, currentVolumesResponse) ->
legacyVolumes = legacyVolumesResponse.Volumes ? []
currentVolumes = currentVolumesResponse.Volumes ? []
return _.unionBy(legacyVolumes, currentVolumes, 'Name')
)
getAll: => getAll: =>
@docker.listVolumes(filters: label: [ 'io.resin.supervised' ]) @_listWithBothLabels()
.then (response) => .map (volume) =>
volumes = response.Volumes ? [] @docker.getVolume(volume.Name).inspect()
Promise.map volumes, (volume) => .then(@format)
@docker.getVolume(volume.Name).inspect()
.then(@format)
getAllByAppId: (appId) => getAllByAppId: (appId) =>
@getAll() @getAll()

View File

@ -34,7 +34,7 @@ const constants = {
imageCleanupErrorIgnoreTimeout: 3600 * 1000, imageCleanupErrorIgnoreTimeout: 3600 * 1000,
maxDeltaDownloads: 3, maxDeltaDownloads: 3,
defaultVolumeLabels: { defaultVolumeLabels: {
'io.resin.supervised': 'true', 'io.balena.supervised': 'true',
}, },
bootBlockDevice: '/dev/mmcblk0p1', bootBlockDevice: '/dev/mmcblk0p1',
hostConfigVarPrefix: 'RESIN_HOST_', hostConfigVarPrefix: 'RESIN_HOST_',

View File

@ -3,7 +3,7 @@ m = require 'mochainon'
{ Network } = require '../src/compose/network' { Network } = require '../src/compose/network'
describe 'compose/network.coffee', -> describe 'compose/network', ->
describe 'compose config -> internal config', -> describe 'compose config -> internal config', ->
@ -70,6 +70,6 @@ describe 'compose/network.coffee', ->
EnableIPv6: false, EnableIPv6: false,
Internal: false, Internal: false,
Labels: { Labels: {
'io.resin.supervised': 'true' 'io.balena.supervised': 'true'
} }
}) })