1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 15:25:22 +02:00

feat(templates): introduce templates management (#2017)

This commit is contained in:
Anthony Lapenna 2018-07-03 20:31:02 +02:00 committed by GitHub
parent e7939a5384
commit 61c285bd2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 3489 additions and 637 deletions

View file

@ -351,6 +351,43 @@ angular.module('portainer.app', [])
}
};
var templates = {
name: 'portainer.templates',
url: '/templates',
views: {
'content@': {
templateUrl: 'app/portainer/views/templates/templates.html',
controller: 'TemplatesController'
}
},
params: {
key: 'containers',
hide_descriptions: false
}
};
var template = {
name: 'portainer.templates.template',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/portainer/views/templates/edit/template.html',
controller: 'TemplateController'
}
}
};
var templateCreation = {
name: 'portainer.templates.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/portainer/views/templates/create/createtemplate.html',
controller: 'CreateTemplateController'
}
}
};
$stateRegistryProvider.register(root);
$stateRegistryProvider.register(portainer);
$stateRegistryProvider.register(about);
@ -382,4 +419,7 @@ angular.module('portainer.app', [])
$stateRegistryProvider.register(user);
$stateRegistryProvider.register(teams);
$stateRegistryProvider.register(team);
$stateRegistryProvider.register(templates);
$stateRegistryProvider.register(template);
$stateRegistryProvider.register(templateCreation);
}]);

View file

@ -0,0 +1,68 @@
angular.module('portainer.app').component('templateForm', {
templateUrl: 'app/portainer/components/forms/template-form/templateForm.html',
controller: function() {
this.state = {
collapseTemplate: false,
collapseContainer: false,
collapseStack: false,
collapseEnv: false
};
this.addPortBinding = function() {
this.model.Ports.push({ containerPort: '', protocol: 'tcp' });
};
this.removePortBinding = function(index) {
this.model.Ports.splice(index, 1);
};
this.addVolume = function () {
this.model.Volumes.push({ container: '', bind: '', readonly: false, type: 'auto' });
};
this.removeVolume = function(index) {
this.model.Volumes.splice(index, 1);
};
this.addLabel = function () {
this.model.Labels.push({ name: '', value: ''});
};
this.removeLabel = function(index) {
this.model.Labels.splice(index, 1);
};
this.addEnvVar = function() {
this.model.Env.push({ type: 1, name: '', label: '', description: '', default: '', preset: true, select: [] });
};
this.removeEnvVar = function(index) {
this.model.Env.splice(index, 1);
};
this.addEnvVarValue = function(env) {
env.select.push({ name: '', value: '' });
};
this.removeEnvVarValue = function(env, index) {
env.select.splice(index, 1);
};
this.changeEnvVarType = function(env) {
if (env.type === 1) {
env.preset = true;
} else if (env.type === 2) {
env.preset = false;
}
};
},
bindings: {
model: '=',
categories: '<',
networks: '<',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
showTypeSelector: '<'
}
});

View file

@ -0,0 +1,569 @@
<form class="form-horizontal" name="templateForm">
<!-- title-input -->
<div class="form-group" ng-class="{ 'has-error': templateForm.template_title.$invalid }">
<label for="template_title" class="col-sm-3 col-lg-2 control-label text-left">Title</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_title" ng-model="$ctrl.model.Title" placeholder="e.g. my-template" required auto-focus>
</div>
</div>
<div class="form-group" ng-show="templateForm.template_title.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="templateForm.template_title.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !title-input -->
<!-- description-input -->
<div class="form-group" ng-class="{ 'has-error': templateForm.template_description.$invalid }">
<label for="template_description" class="col-sm-3 col-lg-2 control-label text-left">Description</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_description" ng-model="$ctrl.model.Description" placeholder="e.g. template description..." required>
</div>
</div>
<div class="form-group" ng-show="templateForm.template_description.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="templateForm.template_description.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !description-input -->
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseTemplate = !$ctrl.state.collapseTemplate">
Template
<span class="small space-left">
<a ng-if="$ctrl.state.collapseTemplate"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseTemplate"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- template-details -->
<div uib-collapse="$ctrl.state.collapseTemplate">
<div ng-if="$ctrl.showTypeSelector">
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="template_container" ng-model="$ctrl.model.Type" ng-value="1">
<label for="template_container">
<div class="boxselector_header">
<i class="fa fa-server" aria-hidden="true" style="margin-right: 2px;"></i>
Container
</div>
<p>Container template</p>
</label>
</div>
<div>
<input type="radio" id="template_swarm_stack" ng-model="$ctrl.model.Type" ng-value="2">
<label for="template_swarm_stack">
<div class="boxselector_header">
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
Swarm stack
</div>
<p>Stack template (Swarm)</p>
</label>
</div>
<div>
<input type="radio" id="template_compose_stack" ng-model="$ctrl.model.Type" ng-value="3">
<label for="template_compose_stack">
<div class="boxselector_header">
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
Compose stack
</div>
<p>Stack template (Compose)</p>
</label>
</div>
</div>
</div>
</div>
<!-- name -->
<div class="form-group">
<label for="template_name" class="col-sm-3 col-lg-2 control-label text-left">
Name
<portainer-tooltip position="bottom" message="Default name that will be associated to the template"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_name" ng-model="$ctrl.model.Name" placeholder="e.g. myApp">
</div>
</div>
<!-- !name -->
<!-- logo -->
<div class="form-group">
<label for="template_logo" class="col-sm-3 col-lg-2 control-label text-left">
Logo URL
<portainer-tooltip position="bottom" message="Recommended size: 60x60"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_logo" ng-model="$ctrl.model.Logo" placeholder="e.g. https://portainer.io/images/logos/nginx.png">
</div>
</div>
<!-- !logo -->
<!-- note -->
<div class="form-group">
<label for="template_note" class="col-sm-3 col-lg-2 control-label text-left">
Note
<portainer-tooltip position="bottom" message="Usage / extra information about the template. Supports HTML."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<textarea class="form-control" name="template_note" ng-model="$ctrl.model.Note" placeholder='You can use this field to specify extra information. <br/> It supports <b>HTML</b>.'></textarea>
</div>
</div>
<!-- !note -->
<!-- platform -->
<div class="form-group">
<label for="template_platform" class="col-sm-3 col-lg-2 control-label text-left">
Platform
</label>
<div class="col-sm-9 col-lg-10">
<select class="form-control" name="template_platform" ng-model="$ctrl.model.Platform">
<option value="">Multi-platform</option>
<option value="linux">Linux</option>
<option value="windows">Windows</option>
</select>
</div>
</div>
<!-- !platform -->
<!-- categories -->
<div class="form-group">
<label for="template_categories" class="col-sm-3 col-lg-2 control-label text-left">
Categories
</label>
<div class="col-sm-9 col-lg-10">
<ui-select multiple tagging tagging-label="(new category)" ng-model="$ctrl.model.Categories" sortable="true" style="width: 300px;" title="Choose a category">
<ui-select-match placeholder="Select categories...">{{ $item }}</ui-select-match>
<ui-select-choices repeat="category in $ctrl.categories | filter:$select.search">
{{ category }}
</ui-select-choices>
</ui-select>
</div>
</div>
<!-- !categories -->
<!-- administrator-only -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Administrator template
<portainer-tooltip position="bottom" message="Should this template be only available to administrator users."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.model.AdministratorOnly"><i></i>
</label>
</div>
</div>
<!-- administrator-only -->
</div>
<!-- !template-details -->
<div ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseStack = !$ctrl.state.collapseStack">
Stack
<span class="small space-left">
<a ng-if="$ctrl.state.collapseStack"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseStack"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- stack-details -->
<div uib-collapse="$ctrl.state.collapseStack">
<!-- repository-url -->
<div class="form-group" ng-class="{ 'has-error': templateForm.template_repository_url.$invalid }">
<label for="template_repository_url" class="col-sm-3 col-lg-2 control-label text-left">Repository URL</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_repository_url" ng-model="$ctrl.model.Repository.url" placeholder="https://github.com/portainer/portainer-compose" required>
</div>
</div>
<div class="form-group" ng-show="templateForm.template_repository_url.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="templateForm.template_repository_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !repository-url -->
<!-- composefile-path -->
<div class="form-group">
<label for="template_repository_path" class="col-sm-3 col-lg-2 control-label text-left">
Compose file path
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_repository_path" ng-model="$ctrl.model.Repository.stackfile" placeholder='docker-compose.yml'>
</div>
</div>
<!-- !composefile-path -->
</div>
<!-- !stack-details -->
</div>
<div ng-if="$ctrl.model.Type === 1">
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseContainer = !$ctrl.state.collapseContainer">
Container
<span class="small space-left">
<a ng-if="$ctrl.state.collapseContainer"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseContainer"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- container-details -->
<div uib-collapse="$ctrl.state.collapseContainer">
<por-image-registry
image="$ctrl.model.Image"
registry="$ctrl.model.Registry"
auto-complete="true"
label-class="col-sm-2" input-class="col-sm-10 col-md-4"
></por-image-registry>
<!-- command -->
<div class="form-group">
<label for="template_command" class="col-sm-3 col-lg-2 control-label text-left">
Command
<portainer-tooltip position="bottom" message="The command to run in the container. If not specified, the container will use the default command specified in its Dockerfile."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_command" ng-model="$ctrl.model.Command" placeholder='/bin/bash -c \"echo hello\" && exit 777'>
</div>
</div>
<!-- !command -->
<!-- hostname -->
<div class="form-group">
<label for="template_hostname" class="col-sm-3 col-lg-2 control-label text-left">
Hostname
<portainer-tooltip position="bottom" message="Set the hostname of the container. Will use Docker default if not specified."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="template_hostname" ng-model="$ctrl.model.Hostname" placeholder='mycontainername'>
</div>
</div>
<!-- !hostname -->
<!-- network -->
<div class="form-group">
<label for="template_network" class="col-sm-3 col-lg-2 control-label text-left">
Network
</label>
<div class="col-sm-10">
<select class="form-control" ng-options="net.Name for net in $ctrl.networks" ng-model="$ctrl.model.Network">
<option disabled hidden value="">Select a network</option>
</select>
</div>
</div>
<!-- !network -->
<!-- port-mapping -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<div class="col-sm-12" style="margin-top: 10px" ng-if="$ctrl.model.Ports.length > 0">
<span class="small text-muted">Portainer will automatically assign a port if you leave the host port empty.</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in $ctrl.model.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
</div>
<!-- !container-port -->
<!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<!-- volumes -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Volume mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
</span>
</div>
<div class="col-sm-12" style="margin-top: 10px" ng-if="$ctrl.model.Volumes.length > 0">
<span class="small text-muted">Portainer will automatically create and map a local volume when using the <b>auto</b> option.</span>
</div>
<div ng-repeat="volume in $ctrl.model.Volumes">
<div class="col-sm-12" style="margin-top: 10px;">
<!-- volume-line1 -->
<div class="col-sm-12 form-inline">
<!-- container-path -->
<div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.container" placeholder="e.g. /path/in/container">
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.bind = ''">Auto</label>
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.bind = ''">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !volume-type -->
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;" ng-if="volume.type !== 'auto'">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.bind" placeholder="e.g. /path/on/host">
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</div>
</div>
</div>
<!-- !volumes -->
<!-- labels -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Labels</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in $ctrl.model.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels -->
<!-- restart_policy -->
<div class="form-group">
<label for="template_restart_policy" class="col-sm-3 col-lg-2 control-label text-left">
Restart policy
</label>
<div class="col-sm-9 col-lg-10">
<select class="form-control" name="template_platform" ng-model="$ctrl.model.RestartPolicy">
<option value="always">Always</option>
<option value="unless-stopped">Unless stopped</option>
<option value="on-failure">On failure</option>
<option value="no">None</option>
</select>
</div>
</div>
<!-- !restart_policy -->
<!-- privileged-mode -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Privileged mode
<portainer-tooltip position="bottom" message="Start the container in privileged mode."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.model.Privileged"><i></i>
</label>
</div>
</div>
<!-- !privileged-mode -->
<!-- interactive-mode -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
Interactive mode
<portainer-tooltip position="bottom" message="Start the container in foreground (equivalent of -i -t flags)."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.model.Interactive"><i></i>
</label>
</div>
</div>
<!-- !interactive-mode -->
</div>
<!-- !container-details -->
</div>
<div class="col-sm-12 form-section-title interactive" ng-click="$ctrl.state.collapseEnv = !$ctrl.state.collapseEnv">
Environment
<span class="small space-left">
<a ng-if="$ctrl.state.collapseEnv"><i class="fa fa-plus" aria-hidden="true"></i> expand</a>
<a ng-if="!$ctrl.state.collapseEnv"><i class="fa fa-minus" aria-hidden="true"></i> collapse</a>
</span>
</div>
<!-- environment-details -->
<div uib-collapse="$ctrl.state.collapseEnv">
<!-- env -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVar()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add variable
</span>
</div>
<!-- env-var-list -->
<div style="margin-top: 10px;">
<div class="col-sm-12 template-envvar" ng-repeat="var in $ctrl.model.Env" style="margin-top: 10px;">
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="preset_var_{{$index}}" ng-model="var.type" ng-value="1" ng-change="$ctrl.changeEnvVarType(var)">
<label for="preset_var_{{$index}}">
<div class="boxselector_header">
<i class="fa fa-user-slash" aria-hidden="true" style="margin-right: 2px;"></i>
Preset
</div>
<p>Preset variable</p>
</label>
</div>
<div>
<input type="radio" id="text_var_{{$index}}" ng-model="var.type" ng-value="2" ng-change="$ctrl.changeEnvVarType(var)">
<label for="text_var_{{$index}}">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Text
</div>
<p>Free text value</p>
</label>
</div>
<div>
<input type="radio" id="select_var_{{$index}}" ng-model="var.type" ng-value="3">
<label for="select_var_{{$index}}">
<div class="boxselector_header">
<i class="fa fa-list-ol" aria-hidden="true" style="margin-right: 2px;"></i>
Select
</div>
<p>Choose value from list</p>
</label>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label text-left">
Name
</label>
<div class="col-sm-8">
<input type="text" class="form-control" ng-model="var.name" placeholder="env_var">
</div>
<div class="col-sm-2">
<button class="btn btn-sm btn-danger space-left" type="button" ng-click="$ctrl.removeEnvVar($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<div ng-if="var.type == 2 || var.type == 3">
<div class="form-group">
<label class="col-sm-2 control-label text-left">
Label
</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="var.label" placeholder="Choose a label">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label text-left" style="margin-top: 2px;">
Description
</label>
<div class="col-sm-10" style="margin-top: 2px;">
<input type="text" class="form-control" ng-model="var.description" placeholder="Tooltip">
</div>
</div>
</div>
<div class="form-group" ng-if="var.type === 1 || var.type === 2">
<label class="col-sm-2 control-label text-left">
Default value
</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="var.default" placeholder="default_value">
</div>
</div>
<div ng-if="var.type === 3" style="margin-bottom: 5px;" class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Values</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addEnvVarValue(var)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add allowed value
</span>
</div>
<!-- envvar-values-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="val in var.select" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="val.text" placeholder="Yes, I agree">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="val.value" placeholder="Y">
</div>
<div class="input-group col-sm-1 input-group-sm">
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeEnvVarValue(var, $index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
<input style="margin-left: 5px;" type="checkbox" ng-model="val.default" id="val_default_{{$index}}"><label for="val_default_{{$index}}" class="space-left">Default</label>
</div>
</div>
</div>
</div>
<!-- envvar-values-list -->
</div>
<div class="col-sm-12" ng-show="$ctrl.model.Env.length > 1">
<div class="line-separator"></div>
</div>
</div>
</div>
<!-- !env-var-list -->
</div>
<!-- !env -->
</div>
<!-- !environment-details -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.formAction()" ng-disabled="$ctrl.actionInProgress || !templateForm.$valid" button-spinner="$ctrl.actionInProgress">
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>

View file

@ -0,0 +1,10 @@
angular.module('portainer.app').component('templateItem', {
templateUrl: 'app/portainer/components/template-list/template-item/templateItem.html',
bindings: {
model: '=',
onSelect: '<',
onDelete: '<',
showUpdateAction: '<',
showDeleteAction: '<'
}
});

View file

@ -0,0 +1,56 @@
<!-- template -->
<div ng-class="{ 'template-container--selected': $ctrl.model.Selected }" class="template-container" ng-click="$ctrl.onSelect($ctrl.model)">
<div class="template-main">
<!-- template-image -->
<span ng-if="$ctrl.model.Logo">
<img class="template-logo" ng-src="{{ $ctrl.model.Logo }}" />
</span>
<span class="template-logo" ng-if="!$ctrl.model.Logo">
<i class="fa fa-rocket fa-4x blue-icon" aria-hidden="true"></i>
</span>
<!-- !template-image -->
<!-- template-details -->
<span class="col-sm-12">
<!-- template-line1 -->
<div class="template-line">
<span>
<span class="template-title">
{{ $ctrl.model.Title }}
</span>
<span class="space-left template-type">
<span>
<i class="fab fa-linux" aria-hidden="true" ng-if="$ctrl.model.Platform === 'linux' || !$ctrl.model.Platform"></i>
<span ng-if="!$ctrl.model.Platform"> &amp; </span>
<i class="fab fa-windows" aria-hidden="true" ng-if="$ctrl.model.Platform === 'windows' || !$ctrl.model.Platform"></i>
</span>
<span>
{{ $ctrl.model.Type === 1 ? 'container' : 'stack' }}
</span>
</span>
</span>
<span class="text-small">
<a ui-sref="portainer.templates.template({ id: $ctrl.model.Id })" class="btn btn-xs btn-primary" ng-click="$event.stopPropagation();" ng-if="$ctrl.showUpdateAction">
<i class="fa fa-edit" aria-hidden="true"></i>
Update
</a>
<btn class="btn btn-xs btn-danger" ng-click="$event.stopPropagation(); $ctrl.onDelete($ctrl.model)" ng-if="$ctrl.showDeleteAction">
<i class="fa fa-trash" aria-hidden="true"></i> Delete
</btn>
</span>
</div>
<!-- !template-line1 -->
<!-- template-line2 -->
<div class="template-line">
<span class="template-description">
{{ $ctrl.model.Description }}
</span>
<span class="small text-muted" ng-if="$ctrl.model.Categories.length > 0">
{{ $ctrl.model.Categories.join(', ') }}
</span>
</div>
<!-- !template-line2 -->
</span>
<!-- !template-details -->
</div>
<!-- !template -->
</div>

View file

@ -0,0 +1,59 @@
angular.module('portainer.app').component('templateList', {
templateUrl: 'app/portainer/components/template-list/templateList.html',
controller: function() {
var ctrl = this;
this.state = {
textFilter: '',
selectedCategory: '',
categories: [],
showContainerTemplates: true
};
this.updateCategories = function() {
var availableCategories = [];
for (var i = 0; i < ctrl.templates.length; i++) {
var template = ctrl.templates[i];
if ((template.Type === 1 && ctrl.state.showContainerTemplates) || (template.Type === 2 && ctrl.showSwarmStacks) || (template.Type === 3 && !ctrl.showSwarmStacks)) {
availableCategories = availableCategories.concat(template.Categories);
}
}
this.state.categories = _.sortBy(_.uniq(availableCategories));
};
this.filterByCategory = function(item) {
if (!ctrl.state.selectedCategory) {
return true;
}
return _.includes(item.Categories, ctrl.state.selectedCategory);
};
this.filterByType = function(item) {
if ((item.Type === 1 && ctrl.state.showContainerTemplates) || (item.Type === 2 && ctrl.showSwarmStacks) || (item.Type === 3 && !ctrl.showSwarmStacks)) {
return true;
}
return false;
};
this.$onInit = function() {
if (this.showSwarmStacks) {
this.state.showContainerTemplates = false;
}
this.updateCategories();
};
},
bindings: {
titleText: '@',
titleIcon: '@',
templates: '<',
selectAction: '<',
deleteAction: '<',
showSwarmStacks: '<',
showAddAction: '<',
showUpdateAction: '<',
showDeleteAction: '<'
}
});

View file

@ -0,0 +1,62 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
</div>
</div>
<div class="actionBar">
<div>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.templates.new" ng-if="$ctrl.showAddAction">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add template
</button>
<span ng-class="{ 'pull-right': $ctrl.showAddAction }" style="width: 25%;">
<ui-select ng-model="$ctrl.state.selectedCategory">
<ui-select-match placeholder="Select a category" allow-clear="true">
<span>{{ $select.selected }}</span>
</ui-select-match>
<ui-select-choices repeat="category in ($ctrl.state.categories | filter: $select.search)">
<span>{{ category }}</span>
</ui-select-choices>
</ui-select>
</span>
</div>
<div class="small text-muted" style="margin: 15px 0 0 5px;">
<label for="show_stacks" class="control-label text-left">
Show container templates
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="show_stacks" ng-model="$ctrl.state.showContainerTemplates" ng-change="$ctrl.updateCategories()"><i></i>
</label>
</div>
</div>
<div class="searchBar" style="border-top: 2px solid #f6f6f6;">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
</div>
<div class="template-list">
<template-item
ng-repeat="template in $ctrl.templates | filter: $ctrl.filterByType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter"
model="template"
show-update-action="$ctrl.showUpdateAction"
show-delete-action="$ctrl.showDeleteAction"
on-select="$ctrl.selectAction"
on-delete="$ctrl.deleteAction"
></template-item>
<div ng-if="!$ctrl.templates" class="text-center text-muted">
Loading...
</div>
<div ng-if="($ctrl.templates | filter: $ctrl.filterByType | filter:$ctrl.filterByCategory | filter:$ctrl.state.textFilter).length == 0" class="text-center text-muted">
No templates available.
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -7,7 +7,7 @@ angular.module('portainer.app')
icon: '='
},
transclude: true,
template: '<div class="widget-header"><div class="row"><span class="pull-left"><img class="custom-header-ico" ng-src="{{icon}}"></img> <span class="text-muted"> {{titleText}} </span> </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
template: '<div class="widget-header"><div class="row"><span class="pull-left"><img class="custom-header-ico" ng-src="{{icon}}" ng-if="icon"></img><i class="fa fa-rocket" aria-hidden="true" ng-if="!icon"></i> <span class="text-muted"> {{titleText}} </span> </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
restrict: 'E'
};
return directive;

View file

@ -62,14 +62,6 @@ angular.module('portainer.app')
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);
}
}
env.push(envvar.name + '=' + value);
}
});
@ -90,14 +82,14 @@ angular.module('portainer.app')
helper.createVolumeBindings = function(volumes, generatedVolumesPile) {
volumes.forEach(function (volume) {
if (volume.containerPath) {
if (volume.container) {
var binding;
if (volume.type === 'auto') {
binding = generatedVolumesPile.pop().Id + ':' + volume.containerPath;
} else if (volume.type !== 'auto' && volume.name) {
binding = volume.name + ':' + volume.containerPath;
binding = generatedVolumesPile.pop().Id + ':' + volume.container;
} else if (volume.type !== 'auto' && volume.bind) {
binding = volume.bind + ':' + volume.container;
}
if (volume.readOnly) {
if (volume.readonly) {
binding += ':ro';
}
volume.binding = binding;
@ -115,21 +107,13 @@ angular.module('portainer.app')
return count;
};
helper.filterLinuxServerIOTemplates = function(templates) {
return templates.filter(function f(template) {
var valid = false;
if (template.Categories) {
angular.forEach(template.Categories, function(category) {
if (_.startsWith(category, 'Network')) {
valid = true;
}
});
}
return valid;
}).map(function(template, idx) {
template.index = idx;
return template;
});
helper.getUniqueCategories = function(templates) {
var categories = [];
for (var i = 0; i < templates.length; i++) {
var template = templates[i];
categories = categories.concat(template.Categories);
}
return _.uniq(categories);
};
return helper;

View file

@ -1,8 +1,6 @@
function SettingsViewModel(data) {
this.TemplatesURL = data.TemplatesURL;
this.LogoURL = data.LogoURL;
this.BlackListedLabels = data.BlackListedLabels;
this.DisplayExternalContributors = data.DisplayExternalContributors;
this.AuthenticationMethod = data.AuthenticationMethod;
this.LDAPSettings = data.LDAPSettings;
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;

View file

@ -0,0 +1,145 @@
function TemplateDefaultModel() {
this.Type = 1;
this.AdministratorOnly = false;
this.Title = '';
this.Image = '';
this.Description = '';
this.Volumes = [];
this.Ports = [];
this.Env = [];
this.Labels = [];
this.RestartPolicy = 'always';
this.Registry = {};
}
function TemplateCreateRequest(model) {
this.Type = model.Type;
this.Name = model.Name;
this.Hostname = model.Hostname;
this.Title = model.Title;
this.Description = model.Description;
this.Note = model.Note;
this.Categories = model.Categories;
this.Platform = model.Platform;
this.Logo = model.Logo;
this.Image = model.Image;
this.Registry = model.Registry.URL;
this.Command = model.Command;
this.Network = model.Network;
this.Privileged = model.Privileged;
this.Interactive = model.Interactive;
this.RestartPolicy = model.RestartPolicy;
this.Labels = model.Labels;
this.Repository = model.Repository;
this.Env = model.Env;
this.AdministratorOnly = model.AdministratorOnly;
this.Ports = [];
for (var i = 0; i < model.Ports.length; i++) {
var binding = model.Ports[i];
if (binding.containerPort && binding.protocol) {
var port = binding.hostPort ? binding.hostPort + ':' + binding.containerPort + '/' + binding.protocol: binding.containerPort + '/' + binding.protocol;
this.Ports.push(port);
}
}
this.Volumes = model.Volumes;
}
function TemplateUpdateRequest(model) {
TemplateCreateRequest.call(this, model);
this.id = model.Id;
}
function TemplateViewModel(data) {
this.Id = data.Id;
this.Title = data.title;
this.Type = data.type;
this.Description = data.description;
this.AdministratorOnly = data.AdministratorOnly;
this.Name = data.name;
this.Note = data.note;
this.Categories = data.categories ? data.categories : [];
this.Platform = data.platform ? data.platform : '';
this.Logo = data.logo;
this.Repository = data.repository;
this.Hostname = data.hostname;
this.Registry = data.registry ? { URL: data.registry } : {};
this.Image = data.image;
this.Registry = data.registry ? data.registry : '';
this.Command = data.command ? data.command : '';
this.Network = data.network ? data.network : '';
this.Privileged = data.privileged ? data.privileged : false;
this.Interactive = data.interactive ? data.interactive : false;
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
this.Labels = data.labels ? data.labels : [];
this.Hosts = data.hosts ? data.hosts : [];
this.Env = templateEnv(data);
this.Volumes = templateVolumes(data);
this.Ports = templatePorts(data);
}
function templatePorts(data) {
var ports = [];
if (data.ports) {
ports = data.ports.map(function (p) {
var portAndProtocol = _.split(p, '/');
var hostAndContainerPort = _.split(portAndProtocol[0], ':');
return {
hostPort: hostAndContainerPort.length > 1 ? hostAndContainerPort[0] : undefined,
containerPort: hostAndContainerPort.length > 1 ? hostAndContainerPort[1] : hostAndContainerPort[0],
protocol: portAndProtocol[1]
};
});
}
return ports;
}
function templateVolumes(data) {
var volumes = [];
if (data.volumes) {
volumes = data.volumes.map(function (v) {
return {
container: v.container,
readonly: v.readonly || false,
type: v.bind ? 'bind' : 'auto',
bind : v.bind ? v.bind : null
};
});
}
return volumes;
}
function templateEnv(data) {
var env = [];
if (data.env) {
env = data.env.map(function(envvar) {
envvar.type = 2;
envvar.value = envvar.default ? envvar.default : '';
if (envvar.preset) {
envvar.type = 1;
}
if (envvar.select) {
envvar.type = 3;
for (var i = 0; i < envvar.select.length; i++) {
var allowedValue = envvar.select[i];
if (allowedValue.default) {
envvar.value = allowedValue.value;
break;
}
}
}
return envvar;
});
}
return env;
}

View file

@ -1,6 +1,10 @@
angular.module('portainer.app')
.factory('Template', ['$resource', 'API_ENDPOINT_TEMPLATES', function TemplateFactory($resource, API_ENDPOINT_TEMPLATES) {
return $resource(API_ENDPOINT_TEMPLATES, {}, {
get: {method: 'GET', isArray: true}
.factory('Templates', ['$resource', 'API_ENDPOINT_TEMPLATES', function TemplatesFactory($resource, API_ENDPOINT_TEMPLATES) {
return $resource(API_ENDPOINT_TEMPLATES + '/:id', {}, {
create: { method: 'POST' },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id'} },
update: { method: 'PUT', params: { id: '@id'} },
remove: { method: 'DELETE', params: { id: '@id'} }
});
}]);

View file

@ -1,43 +1,64 @@
angular.module('portainer.app')
.factory('TemplateService', ['$q', 'Template', 'TemplateHelper', 'ImageHelper', 'ContainerHelper', function TemplateServiceFactory($q, Template, TemplateHelper, ImageHelper, ContainerHelper) {
.factory('TemplateService', ['$q', 'Templates', 'TemplateHelper', 'ImageHelper', 'ContainerHelper',
function TemplateServiceFactory($q, Templates, TemplateHelper, ImageHelper, ContainerHelper) {
'use strict';
var service = {};
service.getTemplates = function(key) {
service.templates = function() {
var deferred = $q.defer();
Template.get({key: key}).$promise
Templates.query().$promise
.then(function success(data) {
var templates = data.map(function (tpl, idx) {
var template;
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);
}
template.index = idx;
return template;
var templates = data.map(function (item) {
return new TemplateViewModel(item);
});
if (key === 'linuxserver.io') {
templates = TemplateHelper.filterLinuxServerIOTemplates(templates);
}
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) {
service.template = function(id) {
var deferred = $q.defer();
Templates.get({ id: id }).$promise
.then(function success(data) {
var template = new TemplateViewModel(data);
deferred.resolve(template);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve template details', err: err });
});
return deferred.promise;
};
service.delete = function(id) {
return Templates.remove({ id: id }).$promise;
};
service.create = function(model) {
var payload = new TemplateCreateRequest(model);
return Templates.create(payload).$promise;
};
service.update = function(model) {
var payload = new TemplateUpdateRequest(model);
return Templates.update(payload).$promise;
};
service.createTemplateConfiguration = function(template, containerName, network) {
var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
var containerConfiguration = service.createContainerConfiguration(template, containerName, network);
containerConfiguration.Image = imageConfiguration.fromImage + ':' + imageConfiguration.tag;
return containerConfiguration;
};
service.createContainerConfiguration = function(template, containerName, network, containerMapping) {
service.createContainerConfiguration = function(template, containerName, network) {
var configuration = TemplateHelper.getDefaultContainerConfiguration();
configuration.HostConfig.NetworkMode = network.Name;
configuration.HostConfig.Privileged = template.Privileged;
@ -46,7 +67,7 @@ angular.module('portainer.app')
configuration.name = containerName;
configuration.Hostname = template.Hostname;
configuration.Image = template.Image;
configuration.Env = TemplateHelper.EnvToStringArray(template.Env, containerMapping);
configuration.Env = TemplateHelper.EnvToStringArray(template.Env);
configuration.Cmd = ContainerHelper.commandStringToArray(template.Command);
var portConfiguration = TemplateHelper.portArrayToPortConfiguration(template.Ports);
configuration.HostConfig.PortBindings = portConfiguration.bindings;
@ -63,7 +84,7 @@ angular.module('portainer.app')
TemplateHelper.createVolumeBindings(volumes, generatedVolumesPile);
volumes.forEach(function (volume) {
if (volume.binding) {
configuration.Volumes[volume.containerPath] = {};
configuration.Volumes[volume.container] = {};
configuration.HostConfig.Binds.push(volume.binding);
}
});

View file

@ -25,18 +25,12 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
LocalStorage.storeApplicationState(state.application);
};
manager.updateExternalContributions = function(displayExternalContributors) {
state.application.displayExternalContributors = displayExternalContributors;
LocalStorage.storeApplicationState(state.application);
};
function assignStateFromStatusAndSettings(status, settings) {
state.application.authentication = status.Authentication;
state.application.analytics = status.Analytics;
state.application.endpointManagement = status.EndpointManagement;
state.application.version = status.Version;
state.application.logo = settings.LogoURL;
state.application.displayExternalContributors = settings.DisplayExternalContributors;
state.application.validity = moment().unix();
}

View file

@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title-text="Endpoint group details"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.groups">Groups</a> &gt; <a ui-sref="portainer.groups.group({id: group.Id})">{{ group.Name }}</a>
<a ui-sref="portainer.groups">Groups</a> &gt; {{ ::group.Name }}
</rd-header-content>
</rd-header>

View file

@ -63,47 +63,6 @@
</div>
</div>
<!-- security -->
<!-- app-templates -->
<div class="col-sm-12 form-section-title">
App Templates
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_templates" class="control-label text-left">
Use custom templates
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_templates" ng-model="formValues.customTemplates"><i></i>
</label>
</div>
</div>
<div ng-if="formValues.customTemplates">
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your own template definitions file here. See <a href="https://portainer.readthedocs.io/en/stable/templates.html" target="_blank">Portainer documentation</a> for more details.
</span>
</div>
<div class="form-group" >
<label for="templates_url" class="col-sm-1 control-label text-left">
URL
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json">
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_external_contrib" class="control-label text-left">
Hide external contributions
<portainer-tooltip position="bottom" message="When enabled, external contributions such as LinuxServer.io will not be displayed in the sidebar."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_external_contrib" ng-model="formValues.externalContributions"><i></i>
</label>
</div>
</div>
<!-- !app-templates -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">

View file

@ -8,8 +8,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
$scope.formValues = {
customLogo: false,
customTemplates: false,
externalContributions: false,
restrictBindMounts: false,
restrictPrivilegedMode: false,
labelName: '',
@ -41,11 +39,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
settings.LogoURL = '';
}
if (!$scope.formValues.customTemplates) {
settings.TemplatesURL = DEFAULT_TEMPLATES_URL;
}
settings.DisplayExternalContributors = !$scope.formValues.externalContributions;
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
@ -58,7 +51,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
.then(function success(data) {
Notifications.success('Settings updated');
StateManager.updateLogo(settings.LogoURL);
StateManager.updateExternalContributions(settings.DisplayExternalContributors);
$state.reload();
})
.catch(function error(err) {
@ -77,10 +69,6 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
if (settings.LogoURL !== '') {
$scope.formValues.customLogo = true;
}
if (settings.TemplatesURL !== DEFAULT_TEMPLATES_URL) {
$scope.formValues.customTemplates = true;
}
$scope.formValues.externalContributions = !settings.DisplayExternalContributors;
$scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers;
$scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers;
})

View file

@ -0,0 +1,49 @@
angular.module('portainer.app')
.controller('CreateTemplateController', ['$q', '$scope', '$state', 'TemplateService', 'TemplateHelper', 'NetworkService', 'Notifications',
function ($q, $scope, $state, TemplateService, TemplateHelper, NetworkService, Notifications) {
$scope.state = {
actionInProgress: false
};
$scope.create = function() {
var model = $scope.model;
$scope.state.actionInProgress = true;
TemplateService.create(model)
.then(function success() {
Notifications.success('Template successfully created', model.Title);
$state.go('portainer.templates');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create template');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
function initView() {
$scope.model = new TemplateDefaultModel();
var provider = $scope.applicationState.endpoint.mode.provider;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
$q.all({
templates: TemplateService.templates(),
networks: NetworkService.networks(
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
false,
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
)
})
.then(function success(data) {
$scope.categories = TemplateHelper.getUniqueCategories(data.templates);
$scope.networks = data.networks;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve template details');
});
}
initView();
}]);

View file

@ -0,0 +1,24 @@
<rd-header>
<rd-header-title title-text="Create template"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.templates">Templates</a> &gt; Add template
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<template-form
model="model"
categories="categories"
networks="networks"
form-action="create"
show-type-selector="true"
form-action-label="Create the template"
action-in-progress="state.actionInProgress"
></template-form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,28 @@
<rd-header>
<rd-header-title title-text="Template details">
<a data-toggle="tooltip" title-text="Refresh" ui-sref="portainer.templates.template({id: template.Id})" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.templates">Templates</a> &gt; {{ ::template.Title }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<template-form
model="template"
categories="categories"
networks="networks"
form-action="update"
show-type-selector="false"
form-action-label="Update the template"
action-in-progress="state.actionInProgress"
></template-form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,51 @@
angular.module('portainer.app')
.controller('TemplateController', ['$q', '$scope', '$state', '$transition$', 'TemplateService', 'TemplateHelper', 'NetworkService', 'Notifications',
function ($q, $scope, $state, $transition$, TemplateService, TemplateHelper, NetworkService, Notifications) {
$scope.state = {
actionInProgress: false
};
$scope.update = function() {
var model = $scope.template;
$scope.state.actionInProgress = true;
TemplateService.update(model)
.then(function success() {
Notifications.success('Template successfully updated', model.Title);
$state.go('portainer.templates');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update template');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
function initView() {
var provider = $scope.applicationState.endpoint.mode.provider;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
var templateId = $transition$.params().id;
$q.all({
templates: TemplateService.templates(),
template: TemplateService.template(templateId),
networks: NetworkService.networks(
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
false,
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
)
})
.then(function success(data) {
$scope.categories = TemplateHelper.getUniqueCategories(data.templates);
$scope.template = data.template;
$scope.networks = data.networks;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve template details');
});
}
initView();
}]);

View file

@ -0,0 +1,362 @@
<rd-header id="view-top">
<rd-header-title title-text="Application templates list">
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.templates" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Templates</rd-header-content>
</rd-header>
<div class="row">
<!-- stack-form -->
<div class="col-sm-12" ng-if="state.selectedTemplate && (state.selectedTemplate.Type === 2 || state.selectedTemplate.Type === 3)">
<rd-widget>
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title-text="state.selectedTemplate.Title"></rd-widget-custom-header>
<rd-widget-body classes="padding">
<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.preset || var.select" 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 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="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.name" ng-click="createTemplate()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the stack</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="unselectTemplate(state.selectedTemplate)">Hide</button>
<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.selectedTemplate.Type === 1">
<rd-widget>
<rd-widget-custom-header icon="state.selectedTemplate.Logo" title-text="state.selectedTemplate.Image"></rd-widget-custom-header>
<rd-widget-body classes="padding">
<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. web (optional)">
</div>
</div>
<!-- !name-input -->
<!-- network-input -->
<div class="form-group">
<label for="container_network" class="col-sm-2 control-label text-left">Network</label>
<div class="col-sm-10">
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
<option disabled hidden value="">Select a network</option>
</select>
</div>
</div>
<!-- !network-input -->
<!-- env -->
<div ng-repeat="var in state.selectedTemplate.Env" ng-if="!var.preset || var.select" 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 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 -->
<div class="form-group">
<div class="col-sm-12">
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
<i class="fa fa-plus space-right" aria-hidden="true"></i> Show advanced options
</a>
<a class="small interactive" ng-if="state.showAdvancedOptions" ng-click="state.showAdvancedOptions = false;">
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide advanced options
</a>
</div>
</div>
<div ng-if="state.showAdvancedOptions">
<!-- port-mapping -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<div class="col-sm-12" style="margin-top: 10px" ng-if="state.selectedTemplate.Ports.length > 0">
<span class="small text-muted">Portainer will automatically assign a port if you leave the host port empty.</span>
</div>
<!-- !port-mapping -->
<!-- port-mapping-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in state.selectedTemplate.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
</div>
<!-- !container-port -->
<!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
<!-- volume-mapping -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Volume mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
</span>
</div>
<div class="col-sm-12" style="margin-top: 10px" ng-if="state.selectedTemplate.Volumes.length > 0">
<span class="small text-muted">Portainer will automatically create and map a local volume when using the <b>auto</b> option.</span>
</div>
<div ng-repeat="volume in state.selectedTemplate.Volumes">
<div class="col-sm-12" style="margin-top: 10px;">
<!-- volume-line1 -->
<div class="col-sm-12 form-inline">
<!-- container-path -->
<div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.container" placeholder="e.g. /path/in/container">
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'auto'" ng-click="volume.bind = ''">Auto</label>
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.bind = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.bind = ''" ng-if="isAdmin || allowBindMounts">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !volume-type -->
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;" ng-if="volume.type !== 'auto'">
<i class="fa fa-long-arrow-alt-right" aria-hidden="true"></i>
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'volume'">
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.bind">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
</select>
</div>
<!-- !volume -->
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.bind" placeholder="e.g. /path/on/host">
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.readonly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</div>
</div>
</div>
<!-- !volume-mapping -->
<!-- extra-host -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Hosts file entries</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addExtraHost()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add additional entry
</span>
</div>
<!-- extra-host-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="(idx, host) in state.selectedTemplate.Hosts track by $index" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="state.selectedTemplate.Hosts[idx]" placeholder="e.g. host:IP">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeExtraHost($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<!-- !extra-host -->
<!-- labels -->
<div class="form-group" >
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Labels</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-12">
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in state.selectedTemplate.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels -->
<!-- hostname -->
<div class="form-group">
<label for="container_hostname" class="col-sm-2 control-label text-left">Hostname</label>
<div class="col-sm-10">
<input type="text" name="container_hostname" class="form-control" ng-model="state.selectedTemplate.Hostname" placeholder="leave empty to use docker default">
</div>
</div>
<!-- !hostname -->
</div>
<!-- !advanced-options -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.network" ng-click="createTemplate()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Deploy the container</span>
<span ng-show="state.actionInProgress">Deployment in progress...</span>
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="unselectTemplate(state.selectedTemplate)">Hide</button>
<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>
<!-- container-form -->
</div>
<div class="row">
<div class="col-sm-12">
<template-list ng-if="templates"
title-text="Templates" title-icon="fa-rocket"
templates="templates"
select-action="selectTemplate"
delete-action="deleteTemplate"
show-add-action="isAdmin"
show-update-action="isAdmin"
show-delete-action="isAdmin"
show-swarm-stacks="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER' && applicationState.endpoint.apiVersion >= 1.25"
></template-list>
</div>
</div>

View file

@ -0,0 +1,257 @@
angular.module('portainer.app')
.controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', 'ContainerService', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', 'StackService', 'EndpointProvider', 'ModalService',
function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, ResourceControlService, Authentication, FormValidator, SettingsService, StackService, EndpointProvider, ModalService) {
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false,
formValidationError: '',
actionInProgress: false
};
$scope.formValues = {
network: '',
name: '',
AccessControlData: new AccessControlFormData()
};
$scope.addVolume = function () {
$scope.state.selectedTemplate.Volumes.push({ containerPath: '', bind: '', readonly: false, type: 'auto' });
};
$scope.removeVolume = function(index) {
$scope.state.selectedTemplate.Volumes.splice(index, 1);
};
$scope.addPortBinding = function() {
$scope.state.selectedTemplate.Ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.state.selectedTemplate.Ports.splice(index, 1);
};
$scope.addExtraHost = function() {
$scope.state.selectedTemplate.Hosts.push('');
};
$scope.removeExtraHost = function(index) {
$scope.state.selectedTemplate.Hosts.splice(index, 1);
};
$scope.addLabel = function () {
$scope.state.selectedTemplate.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.state.selectedTemplate.Labels.splice(index, 1);
};
function validateForm(accessControlData, isAdmin) {
$scope.state.formValidationError = '';
var error = '';
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
$scope.state.formValidationError = error;
return false;
}
return true;
}
function createContainerFromTemplate(template, userId, accessControlData) {
var templateConfiguration = createTemplateConfiguration(template);
var generatedVolumeCount = TemplateHelper.determineRequiredGeneratedVolumeCount(template.Volumes);
var generatedVolumeIds = [];
VolumeService.createXAutoGeneratedLocalVolumes(generatedVolumeCount)
.then(function success(data) {
var volumeResourceControlQueries = [];
angular.forEach(data, function (volume) {
var volumeId = volume.Id;
generatedVolumeIds.push(volumeId);
});
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration, template, data);
return ImageService.pullImage(template.Image, { URL: template.Registry }, true);
})
.then(function success(data) {
return ContainerService.createAndStartContainer(templateConfiguration);
})
.then(function success(data) {
var containerIdentifier = data.Id;
return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, generatedVolumeIds);
})
.then(function success() {
Notifications.success('Container successfully created');
$state.go('docker.containers', {}, {reload: true});
})
.catch(function error(err) {
Notifications.error('Failure', err, err.msg);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
function createComposeStackFromTemplate(template, userId, accessControlData) {
var stackName = $scope.formValues.name;
var repositoryOptions = {
RepositoryURL: template.Repository.url,
ComposeFilePathInRepository: template.Repository.stackfile
};
var endpointId = EndpointProvider.endpointID();
StackService.createComposeStackFromGitRepository(stackName, repositoryOptions, endpointId)
.then(function success(data) {
return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []);
})
.then(function success() {
Notifications.success('Stack successfully deployed');
$state.go('portainer.stacks');
})
.catch(function error(err) {
Notifications.warning('Deployment error', err.data.err);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
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;
}
}
var repositoryOptions = {
RepositoryURL: template.Repository.url,
ComposeFilePathInRepository: template.Repository.stackfile
};
var endpointId = EndpointProvider.endpointID();
StackService.createSwarmStackFromGitRepository(stackName, repositoryOptions, template.Env, endpointId)
.then(function success(data) {
return ResourceControlService.applyResourceControl('stack', stackName, userId, accessControlData, []);
})
.then(function success() {
Notifications.success('Stack successfully deployed');
$state.go('portainer.stacks');
})
.catch(function error(err) {
Notifications.warning('Deployment error', err.err.data.err);
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
}
$scope.createTemplate = function() {
var userDetails = Authentication.getUserDetails();
var userId = userDetails.ID;
var accessControlData = $scope.formValues.AccessControlData;
var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) {
return;
}
var template = $scope.state.selectedTemplate;
var templatesKey = $scope.templatesKey;
$scope.state.actionInProgress = true;
if (template.Type === 2) {
createStackFromTemplate(template, userId, accessControlData);
} if (template.Type === 3) {
createComposeStackFromTemplate(template, userId, accessControlData);
} else {
createContainerFromTemplate(template, userId, accessControlData);
}
};
$scope.unselectTemplate = function(template) {
template.Selected = false;
$scope.state.selectedTemplate = null;
};
$scope.selectTemplate = function(template) {
if ($scope.state.selectedTemplate) {
$scope.unselectTemplate($scope.state.selectedTemplate);
}
template.Selected = true;
if (template.Network) {
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === template.Network; });
} else {
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === 'bridge'; });
}
$scope.formValues.name = template.Name ? template.Name : '';
$scope.state.selectedTemplate = template;
$anchorScroll('view-top');
};
function createTemplateConfiguration(template) {
var network = $scope.formValues.network;
var name = $scope.formValues.name;
return TemplateService.createTemplateConfiguration(template, name, network);
}
$scope.deleteTemplate = function(template) {
ModalService.confirmDeletion(
'Do you want to delete this template?',
function onConfirm(confirmed) {
if(!confirmed) { return; }
deleteTemplate(template);
}
);
};
function deleteTemplate(template) {
TemplateService.delete(template.Id)
.then(function success() {
Notifications.success('Template successfully deleted');
var idx = $scope.templates.indexOf(template);
$scope.templates.splice(idx, 1);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove template');
});
}
function initView() {
var userDetails = Authentication.getUserDetails();
$scope.isAdmin = userDetails.role === 1;
var endpointMode = $scope.applicationState.endpoint.mode;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
$q.all({
templates: TemplateService.templates(),
volumes: VolumeService.getVolumes(),
networks: NetworkService.networks(
endpointMode.provider === 'DOCKER_STANDALONE' || endpointMode.provider === 'DOCKER_SWARM_MODE',
false,
endpointMode.provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25
),
settings: SettingsService.publicSettings()
})
.then(function success(data) {
var templates = data.templates;
$scope.templates = templates;
$scope.availableVolumes = data.volumes.Volumes;
var networks = data.networks;
$scope.availableNetworks = networks;
var settings = data.settings;
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
})
.catch(function error(err) {
$scope.templates = [];
Notifications.error('Failure', err, 'An error occured during apps initialization.');
});
}
initView();
}]);