diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index 759162b72..107115f21 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -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; }]); diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 73fb8e17b..fe41674a6 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -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) { diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index a2d688d84..881053077 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -68,7 +68,10 @@
- + publish a new network port @@ -79,7 +82,7 @@
host - +
@@ -88,7 +91,7 @@
container - +