feat(templates): add support for stack templates (#1346)

pull/1360/head
Anthony Lapenna 2017-11-07 08:18:23 +01:00 committed by GitHub
parent 1b6b4733bd
commit 9ceb3a8051
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 275 additions and 87 deletions

View File

@ -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{},

View File

@ -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
}

View File

@ -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;

View File

@ -3,14 +3,79 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadTemplatesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>Templates</rd-header-content>
</rd-header>
<div class="row" style="height: 90%">
<div class="row">
<!-- stack-form -->
<div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'stack'">
<rd-widget>
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title="state.selectedTemplate.Title">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-primary" ng-click="unselectTemplate()">Hide</button>
</div>
</rd-widget-custom-header>
<rd-widget-body classes="padding">
<div class="col-sm-12" ng-if="state.selectedTemplate">
<form class="form-horizontal">
<!-- description -->
<div ng-if="state.selectedTemplate.Note">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<div class="col-sm-12">
<div class="template-note" ng-if="state.selectedTemplate.Note" ng-bind-html="state.selectedTemplate.Note"></div>
</div>
</div>
</div>
<!-- !description -->
<div class="col-sm-12 form-section-title">
Configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-10">
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. myStack" required>
</div>
</div>
<!-- !name-input -->
<!-- env -->
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="var.label && !var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}<portainer-tooltip ng-if="var.description" position="bottom" message="{{ var.description }}"></portainer-tooltip></label>
<div class="col-sm-10">
<!-- <input ng-if="!var.values && (!var.type || !var.type === 'container')" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}"> -->
<input type="text" class="form-control" ng-if="!var.select" ng-model="var.value" id="field_{{ $index }}">
<select class="form-control" ng-if="var.select" ng-model="var.value" id="field_{{ $index }}">
<option selected disabled hidden value="">Select value</option>
<option ng-repeat="choice in var.select" value="{{ choice.value }}">{{ choice.text }}</option>
</select>
</div>
</div>
<!-- !env -->
<!-- access-control -->
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
<!-- !access-control -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.name" ng-click="createTemplate()">Create</button>
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
<!-- !stack-form -->
<!-- container-form -->
<div class="col-sm-12" ng-if="state.selectedTemplate && state.filters.Type === 'container'">
<rd-widget>
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title="state.selectedTemplate.Image">
<div class="pull-right">
@ -225,14 +290,10 @@
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="small text-muted" style="margin-left: 10px" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && !state.formValidationError">
When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.
</span>
<span ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && !state.formValidationError" style="margin-left: 10px">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted" style="margin-left: 5px;">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.</span>
</span>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
@ -242,7 +303,11 @@
</rd-widget-body>
</rd-widget>
</div>
<!-- container-form -->
</div>
<div class="row">
<div class="col-sm-12" style="height: 100%">
<rd-template-widget>
<rd-widget-header icon="fa-rocket" title="Templates">
@ -273,53 +338,92 @@
</div>
</rd-widget-taskbar>
<rd-widget-body classes="padding template-widget-body">
<div class="template-list">
<!-- template -->
<div ng-repeat="tpl in templates | filter:state.filters:true" class="template-container" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index, $index)">
<div class="template-main">
<!-- template-image -->
<span class="">
<img class="template-logo" ng-src="{{ tpl.Logo }}" />
</span>
<!-- !template-image -->
<!-- template-details -->
<span class="col-sm-12">
<!-- template-line1 -->
<div class="template-line">
<span class="template-title">
{{ tpl.Title }}
</span>
<span>
<i class="fa fa-windows" aria-hidden="true" ng-if="tpl.Platform === 'windows'"></i>
<i class="fa fa-linux" aria-hidden="true" ng-if="tpl.Platform === 'linux'"></i>
<!-- Arch / Platform -->
</span>
</div>
<!-- !template-line1 -->
<!-- template-line2 -->
<div class="template-line">
<span class="template-description">
{{ tpl.Description }}
</span>
<span class="small text-muted" ng-if="tpl.Categories.length > 0">
{{ tpl.Categories.join(', ') }}
</span>
</div>
<!-- !template-line2 -->
</span>
<!-- !template-details -->
<form class="form-horizontal">
<div ng-if="templatesKey !== 'linuxserver.io' && state.showDeploymentSelector">
<div class="col-sm-12 form-section-title">
Deployment method
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-click="updateCategories(templates, state.filters.Type)">
<input type="radio" id="registry_quay" ng-model="state.filters.Type" value="stack">
<label for="registry_quay">
<div class="boxselector_header">
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
Stack
</div>
<p>Multi-containers deployment</p>
</label>
</div>
<div ng-click="updateCategories(templates, state.filters.Type)">
<input type="radio" id="registry_custom" ng-model="state.filters.Type" value="container">
<label for="registry_custom">
<div class="boxselector_header">
<i class="fa fa-server" aria-hidden="true" style="margin-right: 2px;"></i>
Container
</div>
<p>Single container deployment</p>
</label>
</div>
</div>
</div>
<!-- !template -->
</div>
<div ng-if="!templates" class="text-center text-muted">
Loading...
<div ng-if="templatesKey !== 'linuxserver.io' && state.showDeploymentSelector">
<div class="col-sm-12 form-section-title">
Templates
</div>
<div class="form-group"></div>
</div>
<div ng-if="(templates | filter:state.filters:true).length == 0" class="text-center text-muted">
No templates available.
<div class="template-list">
<!-- template -->
<div ng-repeat="tpl in templates | filter:state.filters:true" class="template-container" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index, $index)">
<div class="template-main">
<!-- template-image -->
<span class="">
<img class="template-logo" ng-src="{{ tpl.Logo }}" />
</span>
<!-- !template-image -->
<!-- template-details -->
<span class="col-sm-12">
<!-- template-line1 -->
<div class="template-line">
<span class="template-title">
{{ tpl.Title }}
</span>
<span>
<i class="fa fa-windows" aria-hidden="true" ng-if="tpl.Platform === 'windows'"></i>
<i class="fa fa-linux" aria-hidden="true" ng-if="tpl.Platform === 'linux'"></i>
<!-- Arch / Platform -->
</span>
</div>
<!-- !template-line1 -->
<!-- template-line2 -->
<div class="template-line">
<span class="template-description">
{{ tpl.Description }}
</span>
<span class="small text-muted" ng-if="tpl.Categories.length > 0">
{{ tpl.Categories.join(', ') }}
</span>
</div>
<!-- !template-line2 -->
</span>
<!-- !template-details -->
</div>
<!-- !template -->
</div>
<div ng-if="!templates" class="text-center text-muted">
Loading...
</div>
<div ng-if="(templates | filter:state.filters:true).length == 0" class="text-center text-muted">
No templates available.
</div>
</div>
</div>
</form>
</rd-widget-body>
</rd-template-widget>
</div>
</div>

View File

@ -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();
}]);

View File

@ -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 : [];
}

View File

@ -1,4 +1,5 @@
function TemplateViewModel(data) {
this.Type = data.type;
this.Title = data.title;
this.Description = data.description;
this.Note = data.note;

View File

@ -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);

View File

@ -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;