diff --git a/app/app.js b/app/app.js index eb80ea85b..d7774719f 100644 --- a/app/app.js +++ b/app/app.js @@ -244,7 +244,7 @@ angular.module('portainer', [ } }) .state('actions.create.container', { - url: '/container', + url: '/container/:from', views: { 'content@': { templateUrl: 'app/components/createContainer/createcontainer.html', diff --git a/app/components/container/container.html b/app/components/container/container.html index 39069c781..b399a1eca 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -20,6 +20,8 @@ + + diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index 452a835f8..1af934c78 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,6 +1,6 @@ angular.module('container', []) -.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', -function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService) { +.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', +function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) { $scope.activityTime = 0; $scope.portBindings = []; $scope.config = { @@ -196,6 +196,73 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con }); }; + $scope.duplicate = function() { + ModalService.confirmExperimentalFeature(function (experimental) { + if(!experimental) { return; } + $state.go('actions.create.container', {from: $stateParams.id}, {reload: true}); + }); + }; + + $scope.recreate = function() { + ModalService.confirmExperimentalFeature(function (experimental) { + if(!experimental) { return; } + ModalService.confirm({ + title: 'Are you sure ?', + message: 'You\'re about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.', + buttons: { + confirm: { + label: 'Recreate', + className: 'btn-danger' + } + }, + callback: function onConfirm(confirmed) { + if(!confirmed) { return; } + else { + $('#loadingViewSpinner').show(); + var container = $scope.container; + var config = ContainerHelper.configFromContainer(container.Model); + ContainerService.remove(container, true) + .then(function success() { + return RegistryService.retrieveRegistryFromRepository(container.Config.Image); + }) + .then(function success(data) { + return ImageService.pullImage(container.Config.Image, data, true); + }) + .then(function success() { + return ContainerService.createAndStartContainer(config); + }) + .then(function success(data) { + if (!container.ResourceControl) { + return true; + } else { + var containerIdentifier = data.Id; + var resourceControl = container.ResourceControl; + var users = resourceControl.UserAccesses.map(function(u) { + return u.UserId; + }); + var teams = resourceControl.TeamAccesses.map(function(t) { + return t.TeamId; + }); + return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly, + users, teams, containerIdentifier, 'container', []); + } + }) + .then(function success(data) { + Notifications.success('Container successfully re-created'); + $state.go('containers', {}, {reload: true}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to re-create container'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + } + }); + }); + }; + $scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) { $('#joinNetworkSpinner').show(); Network.connect({id: networkId}, { Container: $stateParams.id }, function (d) { diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index bc7163929..ecc556182 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,14 +1,13 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createContainer', []) -.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', -function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', +function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService) { $scope.formValues = { alwaysPull: true, Console: 'none', Volumes: [], - Registry: '', NetworkContainer: '', Labels: [], ExtraHosts: [], @@ -92,6 +91,8 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, $scope.config.HostConfig.Devices.splice(index, 1); }; + $scope.fromContainerMultipleNetworks = false; + function prepareImageConfig(config) { var image = config.Image; var registry = $scope.formValues.Registry; @@ -179,6 +180,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, var networkMode = mode; if (containerName) { networkMode += ':' + containerName; + config.Hostname = ''; } config.HostConfig.NetworkMode = networkMode; @@ -233,6 +235,213 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, return config; } + function confirmCreateContainer() { + var deferred = $q.defer(); + Container.query({ all: 1, filters: {name: ['^/' + $scope.config.name + '$'] }}).$promise + .then(function success(data) { + var existingContainer = data[0]; + if (existingContainer) { + ModalService.confirm({ + title: 'Are you sure ?', + message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?', + buttons: { + confirm: { + label: 'Replace', + className: 'btn-danger' + } + }, + callback: function onConfirm(confirmed) { + if(!confirmed) { deferred.resolve(false); } + else { + // Remove old container + ContainerService.remove(existingContainer, true) + .then(function success(data) { + Notifications.success('Container Removed', existingContainer.Id); + deferred.resolve(true); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove container', err: err }); + }); + } + } + }); + } else { + deferred.resolve(true); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve containers'); + return undefined; + }); + return deferred.promise; + } + + function loadFromContainerCmd(d) { + if ($scope.config.Cmd) { + $scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd); + } else { + $scope.config.Cmd = ''; + } + } + + function loadFromContainerPortBindings(d) { + 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); + } + } + $scope.config.HostConfig.PortBindings = bindings; + } + + function loadFromContainerVolumes(d) { + for (var v in d.Mounts) { + if ({}.hasOwnProperty.call(d.Mounts, v)) { + var mount = d.Mounts[v]; + var volume = { + 'type': mount.Type, + 'name': mount.Name || mount.Source, + 'containerPath': mount.Destination, + 'readOnly': mount.RW === false + }; + $scope.formValues.Volumes.push(volume); + } + } + } + + function loadFromContainerNetworkConfig(d) { + $scope.config.NetworkingConfig = { + EndpointsConfig: {} + }; + var networkMode = d.HostConfig.NetworkMode; + if (networkMode === 'default') { + $scope.config.HostConfig.NetworkMode = 'bridge'; + if (!_.find($scope.availableNetworks, {'Name': 'bridge'})) { + $scope.config.HostConfig.NetworkMode = 'nat'; + } + } + if ($scope.config.HostConfig.NetworkMode.indexOf('container:') === 0) { + var netContainer = $scope.config.HostConfig.NetworkMode.split(/^container:/)[1]; + $scope.config.HostConfig.NetworkMode = 'container'; + for (var c in $scope.runningContainers) { + if ($scope.runningContainers[c].Names && $scope.runningContainers[c].Names[0] === '/' + netContainer) { + $scope.formValues.NetworkContainer = $scope.runningContainers[c]; + } + } + } + $scope.fromContainerMultipleNetworks = Object.keys(d.NetworkSettings.Networks).length >= 2; + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]) { + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig) { + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address) { + $scope.formValues.IPv4 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address; + } + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address) { + $scope.formValues.IPv6 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address; + } + } + } + $scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]; + // ExtraHosts + for (var h in $scope.config.HostConfig.ExtraHosts) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.ExtraHosts, h)) { + $scope.formValues.ExtraHosts.push({'value': $scope.config.HostConfig.ExtraHosts[h]}); + $scope.config.HostConfig.ExtraHosts = []; + } + } + } + + function loadFromContainerEnvrionmentVariables(d) { + var envArr = []; + for (var e in $scope.config.Env) { + if ({}.hasOwnProperty.call($scope.config.Env, e)) { + var arr = $scope.config.Env[e].split(/\=(.+)/); + envArr.push({'name': arr[0], 'value': arr[1]}); + } + } + $scope.config.Env = envArr; + } + + function loadFromContainerLabels(d) { + for (var l in $scope.config.Labels) { + if ({}.hasOwnProperty.call($scope.config.Labels, l)) { + $scope.formValues.Labels.push({ name: l, value: $scope.config.Labels[l]}); + } + } + } + + function loadFromContainerConsole(d) { + if ($scope.config.OpenStdin && $scope.config.Tty) { + $scope.formValues.Console = 'both'; + } else if (!$scope.config.OpenStdin && $scope.config.Tty) { + $scope.formValues.Console = 'tty'; + } else if ($scope.config.OpenStdin && !$scope.config.Tty) { + $scope.formValues.Console = 'interactive'; + } else if (!$scope.config.OpenStdin && !$scope.config.Tty) { + $scope.formValues.Console = 'none'; + } + } + + function loadFromContainerDevices(d) { + var path = []; + for (var dev in $scope.config.HostConfig.Devices) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) { + var device = $scope.config.HostConfig.Devices[dev]; + path.push({'pathOnHost': device.PathOnHost, 'pathInContainer': device.PathInContainer}); + } + } + $scope.config.HostConfig.Devices = path; + } + + function loadFromContainerImageConfig(d) { + // If no registry found, we let default DockerHub and let full image path + var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image); + RegistryService.retrieveRegistryFromRepository($scope.config.Image) + .then(function success(data) { + if (data) { + $scope.config.Image = imageInfo.image; + $scope.formValues.Registry = data; + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrive registry'); + }); + } + + function loadFromContainerSpec() { + // Get container + Container.get({ id: $stateParams.from }).$promise + .then(function success(d) { + var fromContainer = new ContainerDetailsViewModel(d); + if (!fromContainer.ResourceControl) { + $scope.formValues.AccessControlData.AccessControlEnabled = false; + } + $scope.fromContainer = fromContainer; + $scope.config = ContainerHelper.configFromContainer(fromContainer.Model); + loadFromContainerCmd(d); + loadFromContainerPortBindings(d); + loadFromContainerVolumes(d); + loadFromContainerNetworkConfig(d); + loadFromContainerEnvrionmentVariables(d); + loadFromContainerLabels(d); + loadFromContainerConsole(d); + loadFromContainerDevices(d); + loadFromContainerImageConfig(d); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve container'); + }); + } + function initView() { Volume.query({}, function (d) { $scope.availableVolumes = d.Volumes; @@ -264,6 +473,12 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Container.query({}, function (d) { var containers = d; $scope.runningContainers = containers; + if ($stateParams.from !== '') { + loadFromContainerSpec(); + } else { + $scope.fromContainer = {}; + $scope.formValues.Registry = {}; + } }, function(e) { Notifications.error('Failure', e, 'Unable to retrieve running containers'); }); @@ -283,19 +498,27 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, } $scope.create = function () { - $('#createContainerSpinner').show(); + confirmCreateContainer() + .then(function success(confirm) { + if (!confirm) { + return false; + } + $('#createContainerSpinner').show(); + var accessControlData = $scope.formValues.AccessControlData; + var userDetails = Authentication.getUserDetails(); + var isAdmin = userDetails.role === 1 ? true : false; - var accessControlData = $scope.formValues.AccessControlData; - var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1 ? true : false; + if (!validateForm(accessControlData, isAdmin)) { + $('#createContainerSpinner').hide(); + return; + } - if (!validateForm(accessControlData, isAdmin)) { - $('#createContainerSpinner').hide(); - return; - } - - var config = prepareConfiguration(); - createContainer(config, accessControlData); + var config = prepareConfiguration(); + createContainer(config, accessControlData); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create container'); + }); }; function createContainer(config, accessControlData) { diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index f633c0ed8..fdae6b4de 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -23,7 +23,7 @@