mirror of https://github.com/portainer/portainer
feat(containers): added support for port range mappings when deploying containers (#3194)
* feat(containers): added support for port range mappings when deploying containers * feat(containers): added placeholders to port publishing input fields * feat(containers): added a tooltip to the manual network port publishing * feat(containers): improved the code consistencypull/3292/head
parent
f67e866e7e
commit
accca0f2a6
|
@ -1,5 +1,66 @@
|
|||
import _ from 'lodash-es';
|
||||
import splitargs from 'splitargs/src/splitargs'
|
||||
|
||||
const portPattern = /^([1-9]|[1-5]?[0-9]{2,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/m;
|
||||
|
||||
function parsePort(port) {
|
||||
if (portPattern.test(port)) {
|
||||
return parseInt(port);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function parsePortRange(portRange) {
|
||||
if (typeof portRange !== 'string') {
|
||||
portRange = portRange.toString();
|
||||
}
|
||||
|
||||
// Split the range and convert to integers
|
||||
const stringPorts = _.split(portRange, '-', 2);
|
||||
const intPorts = _.map(stringPorts, parsePort);
|
||||
|
||||
// If it's not a range, we still make sure that we return two ports (start & end)
|
||||
if (intPorts.length == 1) {
|
||||
intPorts.push(intPorts[0]);
|
||||
}
|
||||
|
||||
return intPorts;
|
||||
}
|
||||
|
||||
function isValidPortRange(portRange) {
|
||||
if (typeof portRange === 'string') {
|
||||
portRange = parsePortRange();
|
||||
}
|
||||
|
||||
return Array.isArray(portRange) && portRange.length === 2 &&
|
||||
portRange[0] > 0 && portRange[1] >= portRange[0];
|
||||
}
|
||||
|
||||
function createPortRange(portRangeText, port) {
|
||||
if (typeof portRangeText !== 'string') {
|
||||
portRangeText = portRangeText.toString();
|
||||
}
|
||||
|
||||
let hostIp = null;
|
||||
const colonIndex = portRangeText.indexOf(':');
|
||||
if (colonIndex >= 0) {
|
||||
hostIp = portRangeText.substr(0, colonIndex);
|
||||
portRangeText = portRangeText.substr(colonIndex + 1);
|
||||
}
|
||||
|
||||
port = (typeof port === 'number' ? port : parsePort(port));
|
||||
const portRange = parsePortRange(portRangeText);
|
||||
const startPort = Math.min(portRange[0], port);
|
||||
const endPort = Math.max(portRange[1], port);
|
||||
|
||||
if (hostIp) {
|
||||
return hostIp + ':' + startPort + '-' + endPort;
|
||||
} else {
|
||||
return startPort + '-' + endPort;
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.factory('ContainerHelper', [function ContainerHelperFactory() {
|
||||
'use strict';
|
||||
|
@ -54,5 +115,124 @@ angular.module('portainer.docker')
|
|||
return config;
|
||||
};
|
||||
|
||||
helper.preparePortBindings = function(portBindings) {
|
||||
const bindings = {};
|
||||
_.forEach(portBindings, (portBinding) => {
|
||||
if (!portBinding.containerPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hostPort = portBinding.hostPort;
|
||||
const containerPortRange = parsePortRange(portBinding.containerPort);
|
||||
if (!isValidPortRange(containerPortRange)) {
|
||||
throw new Error('Invalid port specification: ' + portBinding.containerPort);
|
||||
}
|
||||
|
||||
const startPort = containerPortRange[0];
|
||||
const endPort = containerPortRange[1];
|
||||
let hostIp = undefined;
|
||||
let startHostPort = 0;
|
||||
let endHostPort = 0;
|
||||
if (hostPort) {
|
||||
if (hostPort.indexOf(':') > -1) {
|
||||
const hostAndPort = _.split(hostPort, ':');
|
||||
hostIp = hostAndPort[0];
|
||||
hostPort = hostAndPort[1];
|
||||
}
|
||||
|
||||
const hostPortRange = parsePortRange(hostPort);
|
||||
if (!isValidPortRange(hostPortRange)) {
|
||||
throw new Error('Invalid port specification: ' + hostPort);
|
||||
}
|
||||
|
||||
startHostPort = hostPortRange[0];
|
||||
endHostPort = hostPortRange[1];
|
||||
if (endPort !== startPort && (endPort - startPort) !== (endHostPort - startHostPort)) {
|
||||
throw new Error('Invalid port specification: ' + hostPort);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i <= (endPort - startPort); i++) {
|
||||
const containerPort = (startPort + i).toString();
|
||||
if (startHostPort > 0) {
|
||||
hostPort = (startHostPort + i).toString();
|
||||
}
|
||||
if (startPort === endPort && startHostPort !== endHostPort) {
|
||||
hostPort += '-' + endHostPort.toString();
|
||||
}
|
||||
|
||||
const bindKey = containerPort + '/' + portBinding.protocol;
|
||||
bindings[bindKey] = [{ HostIp: hostIp, HostPort: hostPort }];
|
||||
}
|
||||
});
|
||||
return bindings;
|
||||
};
|
||||
|
||||
helper.sortAndCombinePorts = function(portBindings) {
|
||||
const bindings = [];
|
||||
const portBindingKeys = _.keys(portBindings);
|
||||
|
||||
// Group the port bindings by protocol
|
||||
const portBindingKeysByProtocol = _.groupBy(portBindingKeys, (portKey) => {
|
||||
return _.split(portKey, '/')[1];
|
||||
});
|
||||
|
||||
_.forEach(portBindingKeysByProtocol, (portBindingKeys, protocol) => {
|
||||
// Group the port bindings by host IP
|
||||
const portBindingKeysByHostIp = _.groupBy(portBindingKeys, (portKey) => {
|
||||
const portBinding = portBindings[portKey][0];
|
||||
return portBinding.HostIp || '';
|
||||
});
|
||||
|
||||
_.forEach(portBindingKeysByHostIp, (portBindingKeys) => {
|
||||
// Sort by host port
|
||||
const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => {
|
||||
return parseInt(_.split(portKey, '/')[0]);
|
||||
});
|
||||
|
||||
let previousHostPort = -1;
|
||||
let previousContainerPort = -1;
|
||||
_.forEach(sortedPortBindingKeys, (portKey) => {
|
||||
const portKeySplit = _.split(portKey, '/');
|
||||
const containerPort = parseInt(portKeySplit[0]);
|
||||
const portBinding = portBindings[portKey][0];
|
||||
const hostPort = parsePort(portBinding.HostPort);
|
||||
|
||||
// We only combine single ports, and skip the host port ranges on one container port
|
||||
if (hostPort > 0) {
|
||||
// If we detect consecutive ports, we create a range of them
|
||||
if (bindings.length > 0 && previousHostPort === (hostPort - 1) && previousContainerPort === (containerPort - 1)) {
|
||||
bindings[bindings.length-1].hostPort = createPortRange(bindings[bindings.length-1].hostPort, hostPort);
|
||||
bindings[bindings.length-1].containerPort = createPortRange(bindings[bindings.length-1].containerPort, containerPort);
|
||||
previousHostPort = hostPort;
|
||||
previousContainerPort = containerPort;
|
||||
return;
|
||||
}
|
||||
|
||||
previousHostPort = hostPort;
|
||||
previousContainerPort = containerPort;
|
||||
} else {
|
||||
previousHostPort = -1;
|
||||
previousContainerPort = -1;
|
||||
}
|
||||
|
||||
let bindingHostPort = portBinding.HostPort.toString();
|
||||
if (portBinding.HostIp) {
|
||||
bindingHostPort = portBinding.HostIp + ':' + bindingHostPort;
|
||||
}
|
||||
|
||||
const binding = {
|
||||
hostPort: bindingHostPort,
|
||||
containerPort: containerPort,
|
||||
protocol: protocol
|
||||
};
|
||||
bindings.push(binding);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return bindings;
|
||||
};
|
||||
|
||||
return helper;
|
||||
}]);
|
||||
|
|
|
@ -5,8 +5,8 @@ import { ContainerDetailsViewModel } from '../../../models/container';
|
|||
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$state', '$timeout', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SystemService', 'SettingsService', 'PluginService', 'HttpRequestHelper',
|
||||
function ($q, $scope, $state, $timeout, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SystemService, SettingsService, PluginService, HttpRequestHelper) {
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$async', '$state', '$timeout', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SystemService', 'SettingsService', 'PluginService', 'HttpRequestHelper',
|
||||
function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SystemService, SettingsService, PluginService, HttpRequestHelper) {
|
||||
|
||||
$scope.create = create;
|
||||
|
||||
|
@ -138,25 +138,8 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
}
|
||||
|
||||
function preparePortBindings(config) {
|
||||
var bindings = {};
|
||||
if (config.ExposedPorts === undefined) {
|
||||
config.ExposedPorts = {};
|
||||
}
|
||||
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
||||
if (portBinding.containerPort) {
|
||||
var key = portBinding.containerPort + '/' + portBinding.protocol;
|
||||
var binding = {};
|
||||
if (portBinding.hostPort && portBinding.hostPort.indexOf(':') > -1) {
|
||||
var hostAndPort = portBinding.hostPort.split(':');
|
||||
binding.HostIp = hostAndPort[0];
|
||||
binding.HostPort = hostAndPort[1];
|
||||
} else {
|
||||
binding.HostPort = portBinding.hostPort;
|
||||
}
|
||||
bindings[key] = [binding];
|
||||
config.ExposedPorts[key] = {};
|
||||
}
|
||||
});
|
||||
const bindings = ContainerHelper.preparePortBindings(config.HostConfig.PortBindings);
|
||||
_.forEach(bindings, (_, key) => config.ExposedPorts[key] = {});
|
||||
config.HostConfig.PortBindings = bindings;
|
||||
}
|
||||
|
||||
|
@ -330,22 +313,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
}
|
||||
|
||||
function loadFromContainerPortBindings() {
|
||||
var bindings = [];
|
||||
for (var p in $scope.config.HostConfig.PortBindings) {
|
||||
if ({}.hasOwnProperty.call($scope.config.HostConfig.PortBindings, p)) {
|
||||
var hostPort = '';
|
||||
if ($scope.config.HostConfig.PortBindings[p][0].HostIp) {
|
||||
hostPort = $scope.config.HostConfig.PortBindings[p][0].HostIp + ':';
|
||||
}
|
||||
hostPort += $scope.config.HostConfig.PortBindings[p][0].HostPort;
|
||||
var b = {
|
||||
'hostPort': hostPort,
|
||||
'containerPort': p.split('/')[0],
|
||||
'protocol': p.split('/')[1]
|
||||
};
|
||||
bindings.push(b);
|
||||
}
|
||||
}
|
||||
const bindings = ContainerHelper.sortAndCombinePorts($scope.config.HostConfig.PortBindings);
|
||||
$scope.config.HostConfig.PortBindings = bindings;
|
||||
}
|
||||
|
||||
|
@ -784,8 +752,10 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
|||
}
|
||||
|
||||
function createNewContainer() {
|
||||
var config = prepareConfiguration();
|
||||
return ContainerService.createAndStartContainer(config);
|
||||
return $async(async () => {
|
||||
const config = prepareConfiguration();
|
||||
return await ContainerService.createAndStartContainer(config);
|
||||
});
|
||||
}
|
||||
|
||||
function applyResourceControl(newContainer) {
|
||||
|
|
|
@ -68,7 +68,10 @@
|
|||
<!-- port-mapping -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label class="control-label text-left">Manual network port publishing</label>
|
||||
<label class="control-label text-left">
|
||||
Manual network port publishing
|
||||
<portainer-tooltip position="bottom" message="When a range of ports on the host and a single port on the container is specified, Docker will randomly choose a single available port in the defined range and forward that to the container port."></portainer-tooltip>
|
||||
</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> publish a new network port
|
||||
</span>
|
||||
|
@ -79,7 +82,7 @@
|
|||
<!-- host-port -->
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80, 80-88, ip:80 or ip:80-88 (optional)">
|
||||
</div>
|
||||
<!-- !host-port -->
|
||||
<span style="margin: 0 10px 0 10px;">
|
||||
|
@ -88,7 +91,7 @@
|
|||
<!-- container-port -->
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80 or 80-88">
|
||||
</div>
|
||||
<!-- !container-port -->
|
||||
<!-- protocol-actions -->
|
||||
|
|
Loading…
Reference in New Issue