From 9ceb3a8051fe7f8118b8ae5bd38293c0473541a3 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 7 Nov 2017 08:18:23 +0100 Subject: [PATCH] feat(templates): add support for stack templates (#1346) --- api/cmd/portainer/main.go | 2 +- api/http/handler/templates.go | 7 +- app/components/settings/settingsController.js | 1 + app/components/templates/templates.html | 204 +++++++++++++----- .../templates/templatesController.js | 128 ++++++++--- app/models/api/stackTemplate.js | 11 + app/models/api/template.js | 1 + app/services/templateService.js | 4 +- assets/css/app.css | 4 + 9 files changed, 275 insertions(+), 87 deletions(-) create mode 100644 app/models/api/stackTemplate.js diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 920f0b9fd..60bb071e1 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -127,7 +127,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL if err == portainer.ErrSettingsNotFound { settings := &portainer.Settings{ LogoURL: *flags.Logo, - DisplayExternalContributors: true, + DisplayExternalContributors: false, AuthenticationMethod: portainer.AuthenticationInternal, LDAPSettings: portainer.LDAPSettings{ TLSConfig: portainer.TLSConfiguration{}, diff --git a/api/http/handler/templates.go b/api/http/handler/templates.go index 25e2e288b..83527870b 100644 --- a/api/http/handler/templates.go +++ b/api/http/handler/templates.go @@ -43,16 +43,17 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht } var templatesURL string - if key == "containers" { + switch key { + case "containers": settings, err := handler.SettingsService.Settings() if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } templatesURL = settings.TemplatesURL - } else if key == "linuxserver.io" { + case "linuxserver.io": templatesURL = containerTemplatesURLLinuxServerIo - } else { + default: httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger) return } diff --git a/app/components/settings/settingsController.js b/app/components/settings/settingsController.js index cd038dd35..3c122a053 100644 --- a/app/components/settings/settingsController.js +++ b/app/components/settings/settingsController.js @@ -40,6 +40,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_ if (!$scope.formValues.customTemplates) { settings.TemplatesURL = DEFAULT_TEMPLATES_URL; } + settings.DisplayExternalContributors = !$scope.formValues.externalContributions; settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts; settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode; diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index 74f773fbe..94ffe752c 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -3,14 +3,79 @@ - + Templates -
+
+ +
+ + +
+ +
+
+ -
+
+ +
+
+ Information +
+
+
+
+
+
+
+ +
+ Configuration +
+ +
+ +
+ +
+
+ + +
+ +
+ + + +
+
+ + + + + +
+
+ + + {{ state.formValidationError }} +
+
+ +
+ + + +
+ + +
@@ -225,14 +290,10 @@
- + When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the networks view to create one. - - - App templates cannot be deployed as Swarm Mode services for the moment. You can still use them to quickly deploy containers on the Docker host. - {{ state.formValidationError }}
@@ -242,7 +303,11 @@
+ +
+ +
@@ -273,53 +338,92 @@
-
- -
-
- - - - - - - - -
- - {{ tpl.Title }} - - - - - - -
- - -
- - {{ tpl.Description }} - - - {{ tpl.Categories.join(', ') }} - -
- -
- +
+
+
+ Deployment method +
+
+
+
+
+ + +
+
+ + +
+
-
-
- Loading... + +
+
+ Templates +
+
-
- No templates available. + +
+ +
+
+ + + + + + + + +
+ + {{ tpl.Title }} + + + + + + +
+ + +
+ + {{ tpl.Description }} + + + {{ tpl.Categories.join(', ') }} + +
+ +
+ +
+ +
+
+ Loading... +
+
+ No templates available. +
-
+
-
diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 38fc6a525..8ca34a100 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -1,14 +1,16 @@ angular.module('templates', []) -.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', -function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator, SettingsService) { +.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', 'StackService', +function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator, SettingsService, StackService) { $scope.state = { selectedTemplate: null, showAdvancedOptions: false, hideDescriptions: $transition$.params().hide_descriptions, formValidationError: '', + showDeploymentSelector: false, filters: { Categories: '!', - Platform: '!' + Platform: '!', + Type: 'container' } }; @@ -54,19 +56,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer return true; } - $scope.createTemplate = function() { - $('#createContainerSpinner').show(); - - var userDetails = Authentication.getUserDetails(); - var accessControlData = $scope.formValues.AccessControlData; - var isAdmin = userDetails.role === 1 ? true : false; - - if (!validateForm(accessControlData, isAdmin)) { - $('#createContainerSpinner').hide(); - return; - } - - var template = $scope.state.selectedTemplate; + function createContainerFromTemplate(template, userId, accessControlData) { var templateConfiguration = createTemplateConfiguration(template); var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes); var generatedVolumeIds = []; @@ -85,7 +75,6 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer }) .then(function success(data) { var containerIdentifier = data.Id; - var userId = userDetails.ID; return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, generatedVolumeIds); }) .then(function success() { @@ -96,8 +85,59 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer Notifications.error('Failure', err, err.msg); }) .finally(function final() { - $('#createContainerSpinner').hide(); + $('#createResourceSpinner').hide(); }); + } + + function createStackFromTemplate(template, userId, accessControlData) { + var stackName = $scope.formValues.name; + + for (var i = 0; i < template.Env.length; i++) { + var envvar = template.Env[i]; + if (envvar.set) { + envvar.value = envvar.set; + } + } + + StackService.createStackFromGitRepository(stackName, template.Repository.url, template.Repository.stackfile, template.Env) + .then(function success() { + Notifications.success('Stack successfully created'); + }) + .catch(function error(err) { + Notifications.warning('Deployment error', err.err.data.err); + }) + .then(function success(data) { + return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []); + }) + .then(function success() { + $state.go('stacks', {}, {reload: true}); + }) + .finally(function final() { + $('#createResourceSpinner').hide(); + }); + } + + $scope.createTemplate = function() { + $('#createResourceSpinner').show(); + + var userDetails = Authentication.getUserDetails(); + var userId = userDetails.ID; + var accessControlData = $scope.formValues.AccessControlData; + var isAdmin = userDetails.role === 1 ? true : false; + + if (!validateForm(accessControlData, isAdmin)) { + $('#createResourceSpinner').hide(); + return; + } + + var template = $scope.state.selectedTemplate; + var templatesKey = $scope.templatesKey; + + if (template.Type === 'stack') { + createStackFromTemplate(template, userId, accessControlData); + } else { + createContainerFromTemplate(template, userId, accessControlData); + } }; $scope.unselectTemplate = function() { @@ -152,11 +192,22 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer return containerMapping; } - function initTemplates() { - var templatesKey = $transition$.params().key; - var provider = $scope.applicationState.endpoint.mode.provider; - var apiVersion = $scope.applicationState.endpoint.apiVersion; + $scope.updateCategories = function(templates, type) { + $scope.state.filters.Categories = '!'; + updateCategories(templates, type); + }; + function updateCategories(templates, type) { + var availableCategories = []; + angular.forEach(templates, function(template) { + if (template.Type === type) { + availableCategories = availableCategories.concat(template.Categories); + } + }); + $scope.availableCategories = _.sortBy(_.uniq(availableCategories)); + } + + function initTemplates(templatesKey, type, provider, apiVersion) { $q.all({ templates: TemplateService.getTemplates(templatesKey), containers: ContainerService.containers(0), @@ -169,12 +220,9 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer settings: SettingsService.publicSettings() }) .then(function success(data) { - $scope.templates = data.templates; - var availableCategories = []; - angular.forEach($scope.templates, function(template) { - availableCategories = availableCategories.concat(template.Categories); - }); - $scope.availableCategories = _.sortBy(_.uniq(availableCategories)); + var templates = data.templates; + updateCategories(templates, type); + $scope.templates = templates; $scope.runningContainers = data.containers; $scope.availableVolumes = data.volumes.Volumes; var networks = data.networks; @@ -182,17 +230,33 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer $scope.globalNetworkCount = networks.length; var settings = data.settings; $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers; - var userDetails = Authentication.getUserDetails(); - $scope.isAdmin = userDetails.role === 1 ? true : false; }) .catch(function error(err) { $scope.templates = []; Notifications.error('Failure', err, 'An error occured during apps initialization.'); }) .finally(function final(){ - $('#loadTemplatesSpinner').hide(); + $('#loadingViewSpinner').hide(); }); } - initTemplates(); + function initView() { + var templatesKey = $transition$.params().key; + $scope.templatesKey = templatesKey; + + var userDetails = Authentication.getUserDetails(); + $scope.isAdmin = userDetails.role === 1 ? true : false; + + var endpointMode = $scope.applicationState.endpoint.mode; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + + if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' && apiVersion >= 1.25) { + $scope.state.filters.Type = 'stack'; + $scope.state.showDeploymentSelector = true; + } + + initTemplates(templatesKey, $scope.state.filters.Type, endpointMode.provider, apiVersion); + } + + initView(); }]); diff --git a/app/models/api/stackTemplate.js b/app/models/api/stackTemplate.js new file mode 100644 index 000000000..728d98421 --- /dev/null +++ b/app/models/api/stackTemplate.js @@ -0,0 +1,11 @@ +function StackTemplateViewModel(data) { + this.Type = data.type; + this.Title = data.title; + this.Description = data.description; + this.Note = data.note; + this.Categories = data.categories ? data.categories : []; + this.Platform = data.platform ? data.platform : 'undefined'; + this.Logo = data.logo; + this.Repository = data.repository; + this.Env = data.env ? data.env : []; +} diff --git a/app/models/api/template.js b/app/models/api/template.js index e6499ee33..76a77d961 100644 --- a/app/models/api/template.js +++ b/app/models/api/template.js @@ -1,4 +1,5 @@ function TemplateViewModel(data) { + this.Type = data.type; this.Title = data.title; this.Description = data.description; this.Note = data.note; diff --git a/app/services/templateService.js b/app/services/templateService.js index 57fe4b9b2..89694da59 100644 --- a/app/services/templateService.js +++ b/app/services/templateService.js @@ -9,7 +9,9 @@ angular.module('portainer.services') .then(function success(data) { var templates = data.map(function (tpl, idx) { var template; - if (key === 'linuxserver.io') { + if (tpl.type === 'stack') { + template = new StackTemplateViewModel(tpl); + } else if (tpl.type === 'container' && key === 'linuxserver.io') { template = new TemplateLSIOViewModel(tpl); } else { template = new TemplateViewModel(tpl); diff --git a/assets/css/app.css b/assets/css/app.css index 048acf83c..3f6979afb 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -287,6 +287,10 @@ ul.sidebar .sidebar-title { height: auto; } +ul.sidebar .sidebar-list a { + font-size: 14px; +} + ul.sidebar .sidebar-list a.active { color: #fff; text-indent: 22px;