diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index 57ea3286f..04518aa89 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -10,7 +10,7 @@
- +
@@ -27,9 +27,9 @@
- +
- +
@@ -39,7 +39,7 @@
-
+
@@ -125,9 +125,9 @@
- -
{{ tpl.title }}
-
{{ tpl.description }}
+ +
{{ tpl.Title }}
+
{{ tpl.Description }}
Loading... diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 4619979b9..62d32ae84 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -1,6 +1,6 @@ angular.module('templates', []) -.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Pagination', -function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Pagination) { +.controller('TemplatesController', ['$scope', '$q', '$state', '$anchorScroll', 'Config', 'ContainerService', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Messages', 'Pagination', +function ($scope, $q, $state, $anchorScroll, Config, ContainerService, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Messages, Pagination) { $scope.state = { selectedTemplate: null, showAdvancedOptions: false, @@ -9,233 +9,118 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C $scope.formValues = { network: "", name: "", - ports: [] }; - var selectedItem = -1; - $scope.changePaginationCount = function() { Pagination.setPaginationCount('templates', $scope.state.pagination_count); }; $scope.addPortBinding = function() { - $scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' }); + $scope.state.selectedTemplate.Ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' }); }; $scope.removePortBinding = function(index) { - $scope.formValues.ports.splice(index, 1); + $scope.state.selectedTemplate.Ports.splice(index, 1); }; - // TODO: centralize, already present in createContainerController - function createContainer(config) { - Container.create(config, function (d) { - if (d.message) { - $('#createContainerSpinner').hide(); - Messages.error('Error', {}, d.message); - } else { - Container.start({id: d.Id}, {}, function (cd) { - if (cd.message) { - $('#createContainerSpinner').hide(); - Messages.error('Error', {}, cd.message); - } else { - $('#createContainerSpinner').hide(); - Messages.send('Container Started', d.Id); - $state.go('containers', {}, {reload: true}); - } - }, function (e) { - $('#createContainerSpinner').hide(); - Messages.error("Failure", e, 'Unable to start container'); - }); - } - }, function (e) { - $('#createContainerSpinner').hide(); - Messages.error("Failure", e, 'Unable to create container'); - }); - } - - - // TODO: centralize, already present in createContainerController - function pullImageAndCreateContainer(imageConfig, containerConfig) { - Image.create(imageConfig, function (data) { - var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); - if (err) { - var detail = data[data.length - 1]; - $('#createContainerSpinner').hide(); - Messages.error("Error", {}, detail.error); - } else { - createContainer(containerConfig); - } - }, function (e) { - $('#createContainerSpinner').hide(); - Messages.error("Failure", e, "Unable to pull image"); - }); - } - - function getInitialConfiguration() { - return { - Env: [], - OpenStdin: false, - Tty: false, - ExposedPorts: {}, - HostConfig: { - RestartPolicy: { - Name: 'no' - }, - PortBindings: {}, - Binds: [], - NetworkMode: $scope.formValues.network.Name, - Privileged: false - }, - Volumes: {}, - name: $scope.formValues.name - }; - } - - function preparePortBindings(config, ports) { - var bindings = {}; - ports.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] = {}; - } - }); - config.HostConfig.PortBindings = bindings; - } - - function createConfigFromTemplate(template) { - var containerConfig = getInitialConfiguration(); - containerConfig.Image = template.image; - if (template.env) { - template.env.forEach(function (v) { - if (v.value || v.set) { - var val; - if (v.type && v.type === 'container') { - if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') { - val = $filter('swarmcontainername')(v.value); - } else { - var container = v.value; - val = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress; - } - } else { - val = v.set ? v.set : v.value; - } - containerConfig.Env.push(v.name + "=" + val); - } - }); - } - preparePortBindings(containerConfig, $scope.formValues.ports); - prepareImageConfig(containerConfig, template); - return containerConfig; - } - - function prepareImageConfig(config, template) { - var image = template.image; - var registry = template.registry || ''; - var imageConfig = ImageHelper.createImageConfigForContainer(image, registry); - config.Image = imageConfig.fromImage + ':' + imageConfig.tag; - $scope.imageConfig = imageConfig; - } - - function prepareVolumeQueries(template, containerConfig) { - var volumeQueries = []; - if (template.volumes) { - template.volumes.forEach(function (vol) { - volumeQueries.push( - Volume.create({}, function (d) { - if (d.message) { - Messages.error("Unable to create volume", {}, d.message); - } else { - Messages.send("Volume created", d.Name); - containerConfig.Volumes[vol] = {}; - containerConfig.HostConfig.Binds.push(d.Name + ':' + vol); - } - }, function (e) { - Messages.error("Failure", e, "Unable to create volume"); - }).$promise - ); - }); - } - return volumeQueries; - } - $scope.createTemplate = function() { $('#createContainerSpinner').show(); var template = $scope.state.selectedTemplate; - var containerConfig = createConfigFromTemplate(template); - var createVolumeQueries = prepareVolumeQueries(template, containerConfig); - $q.all(createVolumeQueries).then(function (d) { - pullImageAndCreateContainer($scope.imageConfig, containerConfig); + var templateConfiguration = createTemplateConfiguration(template); + + VolumeService.createAutoGeneratedLocalVolumes(template.Volumes) + .then(function success(data) { + TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data); + return ImageService.pullImage(templateConfiguration.image); + }) + .then(function success(data) { + return ContainerService.createAndStartContainer(templateConfiguration.container); + }) + .then(function success(data) { + Messages.send('Container Started', data.Id); + $state.go('containers', {}, {reload: true}); + }) + .catch(function error(err) { + Messages.error('Failure', err, err.msg); + }) + .finally(function final() { + $('#createContainerSpinner').hide(); }); }; - $scope.selectTemplate = function(id) { - $('#template_' + id).toggleClass("container-template--selected"); - if (selectedItem === id) { - selectedItem = -1; - $scope.state.selectedTemplate = null; + var selectedItem = -1; + $scope.selectTemplate = function(idx) { + $('#template_' + idx).toggleClass("container-template--selected"); + if (selectedItem === idx) { + unselectTemplate(); } else { - $('#template_' + selectedItem).toggleClass("container-template--selected"); - selectedItem = id; - var selectedTemplate = $scope.templates[id]; - $scope.state.selectedTemplate = selectedTemplate; - $scope.formValues.ports = selectedTemplate.ports ? TemplateHelper.getPortBindings(selectedTemplate.ports) : []; - $anchorScroll('selectedTemplate'); + selectTemplate(idx); } }; + function unselectTemplate() { + selectedItem = -1; + $scope.state.selectedTemplate = null; + } + + function selectTemplate(idx) { + $('#template_' + selectedItem).toggleClass("container-template--selected"); + selectedItem = idx; + var selectedTemplate = $scope.templates[idx]; + $scope.state.selectedTemplate = selectedTemplate; + $anchorScroll('selectedTemplate'); + } + + function createTemplateConfiguration(template) { + var network = $scope.formValues.network; + var name = $scope.formValues.name; + var containerMapping = determineContainerMapping(network); + return TemplateService.createTemplateConfiguration(template, name, network, containerMapping); + } + + function determineContainerMapping(network) { + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + var containerMapping = 'BY_CONTAINER_IP'; + if (endpointProvider === 'DOCKER_SWARM' && network.Scope === 'global') { + containerMapping = 'BY_SWARM_CONTAINER_NAME'; + } else if (network.Name !== "bridge") { + containerMapping = 'BY_CONTAINER_NAME'; + } + } + + function filterNetworksBasedOnProvider(networks) { + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + if (endpointProvider === 'DOCKER_SWARM' || endpointProvider === 'DOCKER_SWARM_MODE') { + networks = NetworkService.filterGlobalNetworks(networks); + $scope.globalNetworkCount = networks.length; + NetworkService.addPredefinedLocalNetworks(networks); + } else { + $scope.formValues.network = _.find(networks, function(o) { return o.Name === "bridge"; }); + } + return networks; + } + function initTemplates() { - Templates.get(function (data) { - $scope.templates = data.map(function(tpl,index){ - tpl.index = index; - return tpl; + Config.$promise.then(function (c) { + $q.all({ + templates: TemplateService.getTemplates(), + containers: ContainerService.getContainers(0, c.hiddenLabels), + networks: NetworkService.getNetworks() + }) + .then(function success(data) { + $scope.templates = data.templates; + $scope.runningContainers = data.containers; + $scope.availableNetworks = filterNetworksBasedOnProvider(data.networks); + }) + .catch(function error(err) { + $scope.templates = []; + Messages.error("Failure", err, "An error occured during apps initialization."); + }) + .finally(function final(){ + $('#loadTemplatesSpinner').hide(); }); - $('#loadTemplatesSpinner').hide(); - }, function (e) { - $('#loadTemplatesSpinner').hide(); - Messages.error("Failure", e, "Unable to retrieve apps list"); - $scope.templates = []; }); } - Config.$promise.then(function (c) { - var containersToHideLabels = c.hiddenLabels; - Network.query({}, function (d) { - var networks = d; - if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - networks = d.filter(function (network) { - if (network.Scope === 'global') { - return network; - } - }); - $scope.globalNetworkCount = networks.length; - networks.push({Scope: "local", Name: "bridge"}); - networks.push({Scope: "local", Name: "host"}); - networks.push({Scope: "local", Name: "none"}); - } else { - $scope.formValues.network = _.find(networks, function(o) { return o.Name === "bridge"; }); - } - $scope.availableNetworks = networks; - }, function (e) { - Messages.error("Failure", e, "Unable to retrieve networks"); - }); - Container.query({all: 0}, function (d) { - var containers = d; - if (containersToHideLabels) { - containers = ContainerHelper.hideContainers(d, containersToHideLabels); - } - $scope.runningContainers = containers; - }, function (e) { - Messages.error("Failure", e, "Unable to retrieve running containers"); - }); - initTemplates(); - }); + initTemplates(); }]); diff --git a/app/helpers/templateHelper.js b/app/helpers/templateHelper.js index c52ed25c7..39da37746 100644 --- a/app/helpers/templateHelper.js +++ b/app/helpers/templateHelper.js @@ -1,40 +1,70 @@ angular.module('portainer.helpers') -.factory('TemplateHelper', [function TemplateHelperFactory() { +.factory('TemplateHelper', ['$filter', function TemplateHelperFactory($filter) { 'use strict'; - return { - getPortBindings: function(ports) { - var bindings = []; - ports.forEach(function (port) { - var portAndProtocol = _.split(port, '/'); - var binding = { - containerPort: portAndProtocol[0], - protocol: portAndProtocol[1] - }; - bindings.push(binding); - }); - return bindings; - }, - //Not used atm, may prove useful later - getVolumeBindings: function(volumes) { - var bindings = []; - volumes.forEach(function (volume) { - bindings.push({ containerPath: volume }); - }); - return bindings; - }, - //Not used atm, may prove useful later - getEnvBindings: function(env) { - var bindings = []; - env.forEach(function (envvar) { - var binding = { - name: envvar.name - }; - if (envvar.set) { - binding.value = envvar.set; - } - bindings.push(binding); - }); - return bindings; - } + var helper = {}; + + helper.getDefaultContainerConfiguration = function() { + return { + Env: [], + OpenStdin: false, + Tty: false, + ExposedPorts: {}, + HostConfig: { + RestartPolicy: { + Name: 'no' + }, + PortBindings: {}, + Binds: [], + Privileged: false + }, + Volumes: {} + }; }; + + helper.portArrayToPortConfiguration = function(ports) { + var portConfiguration = { + bindings: {}, + exposedPorts: {} + }; + ports.forEach(function (p) { + if (p.containerPort) { + var key = p.containerPort + "/" + p.protocol; + var binding = {}; + if (p.hostPort) { + binding.HostPort = p.hostPort; + if (p.hostPort.indexOf(':') > -1) { + var hostAndPort = p.hostPort.split(':'); + binding.HostIp = hostAndPort[0]; + binding.HostPort = hostAndPort[1]; + } + } + portConfiguration.bindings[key] = [binding]; + portConfiguration.exposedPorts[key] = {}; + } + }); + return portConfiguration; + }; + + helper.EnvToStringArray = function(templateEnvironment, containerMapping) { + var env = []; + templateEnvironment.forEach(function(envvar) { + if (envvar.value || envvar.set) { + var value = envvar.set ? envvar.set : envvar.value; + if (envvar.type && envvar.type === 'container') { + if (containerMapping === 'BY_CONTAINER_IP') { + var container = envvar.value; + value = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress; + } else if (containerMapping === 'BY_CONTAINER_NAME') { + value = $filter('containername')(envvar.value); + } else if (containerMapping === 'BY_SWARM_CONTAINER_NAME') { + value = $filter('swarmcontainername')(envvar.value); + } + } + env.push(envvar.name + "=" + value); + } + }); + return env; + }; + + return helper; }]); diff --git a/app/models/template.js b/app/models/template.js new file mode 100644 index 000000000..fb0bc70b1 --- /dev/null +++ b/app/models/template.js @@ -0,0 +1,19 @@ +function TemplateViewModel(data) { + this.Title = data.title; + this.Description = data.description; + this.Logo = data.logo; + this.Image = data.image; + this.Registry = data.registry ? data.registry : ''; + this.Env = data.env ? data.env : []; + this.Volumes = data.volumes ? data.volumes : []; + this.Ports = []; + if (data.ports) { + this.Ports = data.ports.map(function (p) { + var portAndProtocol = _.split(p, '/'); + return { + containerPort: portAndProtocol[0], + protocol: portAndProtocol[1] + }; + }); + } +} diff --git a/app/rest/templates.js b/app/rest/template.js similarity index 52% rename from app/rest/templates.js rename to app/rest/template.js index 412e5a9f5..ea02b7ade 100644 --- a/app/rest/templates.js +++ b/app/rest/template.js @@ -1,5 +1,5 @@ angular.module('portainer.rest') -.factory('Templates', ['$resource', 'TEMPLATES_ENDPOINT', function TemplatesFactory($resource, TEMPLATES_ENDPOINT) { +.factory('Template', ['$resource', 'TEMPLATES_ENDPOINT', function TemplateFactory($resource, TEMPLATES_ENDPOINT) { return $resource(TEMPLATES_ENDPOINT, {}, { get: {method: 'GET', isArray: true} }); diff --git a/app/services/containerService.js b/app/services/containerService.js new file mode 100644 index 000000000..c3316c549 --- /dev/null +++ b/app/services/containerService.js @@ -0,0 +1,71 @@ +angular.module('portainer.services') +.factory('ContainerService', ['$q', 'Container', 'ContainerHelper', function ContainerServiceFactory($q, Container, ContainerHelper) { + 'use strict'; + var service = {}; + + service.getContainers = function (all, hiddenLabels) { + var deferred = $q.defer(); + Container.query({ all: all }).$promise + .then(function success(data) { + var containers = data; + if (hiddenLabels) { + containers = ContainerHelper.hideContainers(d, hiddenLabels); + } + deferred.resolve(data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retriever containers', err: err }); + }); + return deferred.promise; + }; + + service.createContainer = function(configuration) { + var deferred = $q.defer(); + Container.create(configuration).$promise + .then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create container', err: err }); + }); + return deferred.promise; + }; + + service.startContainer = function(containerID) { + var deferred = $q.defer(); + Container.start({ id: containerID }, {}).$promise + .then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to start container', err: err }); + }); + return deferred.promise; + }; + + service.createAndStartContainer = function(configuration) { + var deferred = $q.defer(); + var containerID; + service.createContainer(configuration) + .then(function success(data) { + containerID = data.Id; + return service.startContainer(containerID); + }) + .then(function success() { + deferred.resolve({ Id: containerID }); + }) + .catch(function error(err) { + deferred.reject(err); + }); + return deferred.promise; + }; + return service; +}]); diff --git a/app/services/imageService.js b/app/services/imageService.js new file mode 100644 index 000000000..bba649998 --- /dev/null +++ b/app/services/imageService.js @@ -0,0 +1,24 @@ +angular.module('portainer.services') +.factory('ImageService', ['$q', 'Image', function ImageServiceFactory($q, Image) { + 'use strict'; + var service = {}; + + service.pullImage = function(imageConfiguration) { + var deferred = $q.defer(); + Image.create(imageConfiguration).$promise + .then(function success(data) { + var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); + if (err) { + var detail = data[data.length - 1]; + deferred.reject({ msg: detail.error }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to pull image', err: err }); + }); + return deferred.promise; + }; + return service; +}]); diff --git a/app/services/networkService.js b/app/services/networkService.js new file mode 100644 index 000000000..256be1141 --- /dev/null +++ b/app/services/networkService.js @@ -0,0 +1,25 @@ +angular.module('portainer.services') +.factory('NetworkService', ['$q', 'Network', function NetworkServiceFactory($q, Network) { + 'use strict'; + var service = {}; + + service.getNetworks = function() { + return Network.query({}).$promise; + }; + + service.filterGlobalNetworks = function(networks) { + return networks.filter(function (network) { + if (network.Scope === 'global') { + return network; + } + }); + }; + + service.addPredefinedLocalNetworks = function(networks) { + networks.push({Scope: "local", Name: "bridge"}); + networks.push({Scope: "local", Name: "host"}); + networks.push({Scope: "local", Name: "none"}); + }; + + return service; +}]); diff --git a/app/services/templateService.js b/app/services/templateService.js new file mode 100644 index 000000000..99cb2a8da --- /dev/null +++ b/app/services/templateService.js @@ -0,0 +1,59 @@ +angular.module('portainer.services') +.factory('TemplateService', ['$q', 'Template', 'TemplateHelper', 'ImageHelper', function TemplateServiceFactory($q, Template, TemplateHelper, ImageHelper) { + 'use strict'; + var service = {}; + + service.getTemplates = function() { + var deferred = $q.defer(); + Template.get().$promise + .then(function success(data) { + var templates = data.map(function (tpl, idx) { + var template = new TemplateViewModel(tpl); + template.index = idx; + return template; + }); + deferred.resolve(templates); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve templates', err: err }); + }); + return deferred.promise; + }; + + service.createTemplateConfiguration = function(template, containerName, network, containerMapping) { + var imageConfiguration = service.createImageConfiguration(template); + var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping); + containerConfiguration.Image = imageConfiguration.fromImage + ':' + imageConfiguration.tag; + return { + container: containerConfiguration, + image: imageConfiguration + }; + }; + + service.createImageConfiguration = function(template) { + return ImageHelper.createImageConfigForContainer(template.Image, template.Registry); + }; + + service.createContainerConfiguration = function(template, containerName, network, containerMapping) { + var configuration = TemplateHelper.getDefaultContainerConfiguration(); + configuration.HostConfig.NetworkMode = network.Name; + configuration.name = containerName; + configuration.Image = template.Image; + if (template.Env) { + configuration.Env = TemplateHelper.EnvToStringArray(template.Env, containerMapping); + } + var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports); + configuration.HostConfig.PortBindings = portConfiguration.bindings; + configuration.ExposedPorts = portConfiguration.exposedPorts; + return configuration; + }; + + service.updateContainerConfigurationWithVolumes = function(configuration, template, createdVolumes) { + createdVolumes.forEach(function (volume, idx) { + configuration.Volumes[template.Volumes[idx]] = {}; + configuration.HostConfig.Binds.push(volume.Name + ':' + template.Volumes[idx]); + }); + }; + + return service; +}]); diff --git a/app/services/volumeService.js b/app/services/volumeService.js new file mode 100644 index 000000000..ef3debd5b --- /dev/null +++ b/app/services/volumeService.js @@ -0,0 +1,59 @@ +angular.module('portainer.services') +.factory('VolumeService', ['$q', 'Volume', function VolumeServiceFactory($q, Volume) { + 'use strict'; + var service = {}; + + function prepareVolumeQueries(template, containerConfig) { + var volumeQueries = []; + if (template.volumes) { + template.volumes.forEach(function (vol) { + volumeQueries.push( + Volume.create({}, function (d) { + if (d.message) { + Messages.error("Unable to create volume", {}, d.message); + } else { + Messages.send("Volume created", d.Name); + containerConfig.Volumes[vol] = {}; + containerConfig.HostConfig.Binds.push(d.Name + ':' + vol); + } + }, function (e) { + Messages.error("Failure", e, "Unable to create volume"); + }).$promise + ); + }); + } + return volumeQueries; + } + + service.createVolume = function(volumeConfiguration) { + var deferred = $q.defer(); + Volume.create(volumeConfiguration).$promise + .then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create volume', err: err }); + }); + return deferred.promise; + }; + + service.createVolumes = function(volumes) { + var createVolumeQueries = volumes.map(function(volume) { + return service.createVolume(volume); + }); + return $q.all(createVolumeQueries); + }; + + service.createAutoGeneratedLocalVolumes = function (volumes) { + var createVolumeQueries = volumes.map(function(volume) { + return service.createVolume({}); + }); + return $q.all(createVolumeQueries); + }; + + return service; +}]);