mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-24 10:18:29 +00:00
Add fromDockerOpts and normalization to PortMap class, and use in fromContainer
This function takes the docker output representing ports, and generates the port map values from them. This means that services can accurately be compared and next steps can be inferred. The normalization function ensures that regardless of source, PortMaps that represent the same port setup will be represented correctly internally. Change-type: patch Closes: #644 Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
@ -5,6 +5,9 @@ import TypedError = require('typed-error');
|
||||
const PORTS_REGEX =
|
||||
/^(?:(?:([a-fA-F\d.:]+):)?([\d]*)(?:-([\d]+))?:)?([\d]+)(?:-([\d]+))?(?:\/(udp|tcp))?$/;
|
||||
|
||||
// A regex to extract the protocol and internal port of the incoming Docker options
|
||||
const DOCKER_OPTS_PORTS_REGEX = /(\d+)(?:\/?([a-z]+))?/i;
|
||||
|
||||
export class InvalidPortDefinition extends TypedError { }
|
||||
|
||||
export interface PortBindings {
|
||||
@ -16,6 +19,15 @@ export interface DockerPortOptions {
|
||||
portBindings: PortBindings;
|
||||
}
|
||||
|
||||
interface PortRange {
|
||||
internalStart: number;
|
||||
internalEnd: number;
|
||||
externalStart: number;
|
||||
externalEnd: number;
|
||||
protocol: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export class PortMap {
|
||||
|
||||
private internalStart: number;
|
||||
@ -25,8 +37,17 @@ export class PortMap {
|
||||
private protocol: string;
|
||||
private host: string;
|
||||
|
||||
public constructor(portStr: string) {
|
||||
this.parsePortString(portStr);
|
||||
public constructor(portStrOrObj: string | PortRange) {
|
||||
if (_.isString(portStrOrObj)) {
|
||||
this.parsePortString(portStrOrObj);
|
||||
} else {
|
||||
this.internalStart = portStrOrObj.internalStart;
|
||||
this.internalEnd = portStrOrObj.internalEnd;
|
||||
this.externalStart = portStrOrObj.externalStart;
|
||||
this.externalEnd = portStrOrObj.externalEnd;
|
||||
this.protocol = portStrOrObj.protocol;
|
||||
this.host = portStrOrObj.host;
|
||||
}
|
||||
}
|
||||
|
||||
public toDockerOpts(): DockerPortOptions {
|
||||
@ -50,6 +71,75 @@ export class PortMap {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* fromDockerOpts
|
||||
*
|
||||
* We need to be able to compare target and running containers, but
|
||||
* running containers store their ports in a separate format (see the
|
||||
* output of toDockerOpts).
|
||||
*
|
||||
* This function takes the output of toDockerOpts (or more accurately,
|
||||
* the output of the docker daemon when probed about a given container)
|
||||
* and produces a list of PortMap objects, which can then be compared.
|
||||
*
|
||||
*/
|
||||
public static fromDockerOpts(
|
||||
portBindings: PortBindings,
|
||||
): PortMap[] {
|
||||
|
||||
// Create a list of portBindings, rather than the map (which we can't
|
||||
// order)
|
||||
const portMaps = _.map(portBindings, (hostObj, internalStr) => {
|
||||
|
||||
const match = internalStr.match(DOCKER_OPTS_PORTS_REGEX);
|
||||
if (match == null) {
|
||||
throw new Error(`Could not parse docker port output: ${internalStr}`);
|
||||
}
|
||||
const internal = parseInt(match[1], 10);
|
||||
const external = parseInt(hostObj[0].HostPort, 10);
|
||||
|
||||
const host = hostObj[0].HostIp;
|
||||
const proto = match[2] || 'tcp';
|
||||
|
||||
return new PortMap({
|
||||
internalStart: internal,
|
||||
internalEnd: internal,
|
||||
externalStart: external,
|
||||
externalEnd: external,
|
||||
protocol: proto,
|
||||
host,
|
||||
});
|
||||
});
|
||||
|
||||
return PortMap.normalisePortMaps(portMaps);
|
||||
}
|
||||
|
||||
public static normalisePortMaps(portMaps: PortMap[]): PortMap[] {
|
||||
// Fold any ranges into each other if possible
|
||||
return _(portMaps)
|
||||
.sortBy((p) => p.protocol)
|
||||
.sortBy((p) => p.host)
|
||||
.sortBy((p) => p.internalStart)
|
||||
.reduce((res: PortMap[], p: PortMap) => {
|
||||
const last = _.last(res);
|
||||
if (last == null) {
|
||||
res.push(p);
|
||||
} else {
|
||||
if (last.internalEnd + 1 === p.internalStart &&
|
||||
last.externalEnd + 1 === p.externalStart &&
|
||||
last.protocol === p.protocol &&
|
||||
last.host === p.host
|
||||
) {
|
||||
last.internalEnd += 1;
|
||||
last.externalEnd += 1;
|
||||
} else {
|
||||
res.push(p);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
|
||||
private parsePortString(portStr: string): void {
|
||||
const match = portStr.match(PORTS_REGEX);
|
||||
if (match == null) {
|
||||
|
@ -142,8 +142,7 @@ module.exports = class Service
|
||||
@capDrop
|
||||
@status
|
||||
@devices
|
||||
@exposedPorts
|
||||
@portBindings
|
||||
@portMappings
|
||||
@networks
|
||||
@memLimit
|
||||
@memReservation
|
||||
@ -194,8 +193,6 @@ module.exports = class Service
|
||||
@capAdd ?= []
|
||||
@capDrop ?= []
|
||||
@devices ?= []
|
||||
@exposedPorts ?= {}
|
||||
@portBindings ?= {}
|
||||
|
||||
@memLimit = parseMemoryNumber(@memLimit, '0')
|
||||
@memReservation = parseMemoryNumber(@memReservation, '0')
|
||||
@ -240,6 +237,8 @@ module.exports = class Service
|
||||
if _.isEmpty(@ipc)
|
||||
@ipc = 'shareable'
|
||||
|
||||
@portMappings ?= @getPortsAndPortBindings()
|
||||
|
||||
# If the service has no containerId, it is a target service and has to be normalised and extended
|
||||
if !@containerId?
|
||||
if !@networkMode?
|
||||
@ -271,7 +270,6 @@ module.exports = class Service
|
||||
@extendLabels(opts.imageInfo)
|
||||
@extendAndSanitiseVolumes(opts.imageInfo)
|
||||
@extendAndSanitiseExposedPorts(opts.imageInfo)
|
||||
@portMappings = @getPortsAndPortBindings()
|
||||
@devices = formatDevices(@devices)
|
||||
@addFeaturesFromLabels(opts)
|
||||
if @dns?
|
||||
@ -449,6 +447,8 @@ module.exports = class Service
|
||||
if containerPort? and !_.includes(boundContainerPorts, containerPort)
|
||||
expose.push(containerPort)
|
||||
|
||||
portMappings = PortMap.fromDockerOpts(container.HostConfig.PortBindings)
|
||||
|
||||
appId = checkInt(container.Config.Labels['io.resin.app-id'])
|
||||
serviceId = checkInt(container.Config.Labels['io.resin.service-id'])
|
||||
serviceName = container.Config.Labels['io.resin.service-name']
|
||||
@ -483,8 +483,7 @@ module.exports = class Service
|
||||
running: container.State.Running
|
||||
createdAt: new Date(container.Created)
|
||||
restartPolicy: container.HostConfig.RestartPolicy
|
||||
ports: ports
|
||||
expose: expose
|
||||
portMappings: portMappings
|
||||
containerId: container.Id
|
||||
capAdd: container.HostConfig.CapAdd
|
||||
capDrop: container.HostConfig.CapDrop
|
||||
@ -664,9 +663,6 @@ module.exports = class Service
|
||||
_.isEmpty(_.xor(_.keys(@networks), _.keys(otherService.networks)))
|
||||
|
||||
isSameContainer: (otherService) =>
|
||||
# We need computed fields to be present to compare two services
|
||||
@portMappings ?= @getPortsAndPortBindings()
|
||||
otherService.portMappings ?= otherService.getPortsAndPortBindings()
|
||||
|
||||
propertiesToCompare = [
|
||||
'image'
|
||||
|
Reference in New Issue
Block a user